mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-20 04:38:00 +00:00
Compare commits
4432 Commits
forge-1.6.
...
forge-1.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aea32aee62 | ||
|
|
6e8e28a615 | ||
|
|
9ca15ae635 | ||
|
|
bb5102e311 | ||
|
|
4d9b98bc94 | ||
|
|
1e1640b292 | ||
|
|
e250077839 | ||
|
|
5e4c7bce82 | ||
|
|
604010fbce | ||
|
|
4b4bab6086 | ||
|
|
7c38b97540 | ||
|
|
1fa768bb81 | ||
|
|
eda2ba4adf | ||
|
|
001a51a070 | ||
|
|
ac4d476ab3 | ||
|
|
c41c3bd161 | ||
|
|
e2e0123d2b | ||
|
|
7e0ebef57b | ||
|
|
4f53188b94 | ||
|
|
653cbd7345 | ||
|
|
3e980f30aa | ||
|
|
9acc59232b | ||
|
|
33d7596cac | ||
|
|
bded1d604a | ||
|
|
fd1fe2b0e4 | ||
|
|
b5ccfb11e7 | ||
|
|
d35af0a89d | ||
|
|
2462ce58ab | ||
|
|
1d57c1a444 | ||
|
|
3d55461332 | ||
|
|
cdb2474fa1 | ||
|
|
49e5cf7be0 | ||
|
|
3b6ab7e8c1 | ||
|
|
0d15166137 | ||
|
|
1d273d1c70 | ||
|
|
ba540a73ec | ||
|
|
69de2ef5c5 | ||
|
|
b624832293 | ||
|
|
d11bafd244 | ||
|
|
6256a157ab | ||
|
|
43a12342e9 | ||
|
|
3d85051f7f | ||
|
|
c83279ad1f | ||
|
|
e65dcbe6d5 | ||
|
|
5e16d52ce9 | ||
|
|
8331660825 | ||
|
|
fc3cd818b2 | ||
|
|
19ca07e65d | ||
|
|
e903cfc32c | ||
|
|
5be840ab1d | ||
|
|
5567c32a4a | ||
|
|
d3db01cf69 | ||
|
|
1379e06678 | ||
|
|
e4c4a87d45 | ||
|
|
8f10afac00 | ||
|
|
8f8493bd47 | ||
|
|
0d7081a119 | ||
|
|
2ae2f90f5b | ||
|
|
098790cbf3 | ||
|
|
448f723c2c | ||
|
|
924e5bdaf1 | ||
|
|
da81748f54 | ||
|
|
422f8d2c80 | ||
|
|
157aa30918 | ||
|
|
ee87a75621 | ||
|
|
6852748739 | ||
|
|
63bca8ce93 | ||
|
|
0d6fbb60ca | ||
|
|
a670245b3d | ||
|
|
9b27742a5a | ||
|
|
ca7f551af4 | ||
|
|
61040f4c05 | ||
|
|
421da7285c | ||
|
|
6d829db99e | ||
|
|
60d247503e | ||
|
|
50cea02ae1 | ||
|
|
05c3402817 | ||
|
|
34c29be136 | ||
|
|
efa8fa10d6 | ||
|
|
8f93f8e4bc | ||
|
|
fd061354ff | ||
|
|
b7553385f1 | ||
|
|
805048b6b8 | ||
|
|
85ad10a78a | ||
|
|
2cc5bc11c9 | ||
|
|
88ac89a6d2 | ||
|
|
3bb9e96dd0 | ||
|
|
36b62f2219 | ||
|
|
7a52668418 | ||
|
|
22efcf2f3f | ||
|
|
1b95e2a23f | ||
|
|
2131bccae7 | ||
|
|
5c90770e6f | ||
|
|
3adf8a21c9 | ||
|
|
2c049b323a | ||
|
|
c331a045cc | ||
|
|
8a0446add1 | ||
|
|
52190b9175 | ||
|
|
a484b5288e | ||
|
|
483a01b936 | ||
|
|
c21f2203c5 | ||
|
|
7d56940384 | ||
|
|
c19832a559 | ||
|
|
07a0e08730 | ||
|
|
5329fb0b51 | ||
|
|
fc2b5f45a2 | ||
|
|
86ab628cce | ||
|
|
8db68d8717 | ||
|
|
40623420f9 | ||
|
|
d6be6f8b43 | ||
|
|
434acedb8f | ||
|
|
165c95a5d8 | ||
|
|
04dc886c54 | ||
|
|
968407b169 | ||
|
|
8134d72861 | ||
|
|
69e5e4cb21 | ||
|
|
1319e0fda9 | ||
|
|
674d09c06d | ||
|
|
f0833573be | ||
|
|
a2d59cae6a | ||
|
|
53eec599c4 | ||
|
|
e70fa161b4 | ||
|
|
a7a6a3265a | ||
|
|
955c79b640 | ||
|
|
253a4d389c | ||
|
|
7d608ece67 | ||
|
|
e572b4a506 | ||
|
|
0b23d89783 | ||
|
|
5eab3065bc | ||
|
|
5a5cfb30ef | ||
|
|
5b3c22201b | ||
|
|
14ee2f99c8 | ||
|
|
a760901a79 | ||
|
|
2cf101ac7d | ||
|
|
48f0d5b5e4 | ||
|
|
eaaa18b15e | ||
|
|
613bae436d | ||
|
|
3c2f5b2ecf | ||
|
|
15aa393d19 | ||
|
|
f8fda3fb2c | ||
|
|
442263710b | ||
|
|
41013330fc | ||
|
|
64b456827d | ||
|
|
54ad6393ac | ||
|
|
c4a3e93e44 | ||
|
|
d51b9cfa24 | ||
|
|
7250fa7134 | ||
|
|
91b60f1c0f | ||
|
|
3e87e8bb55 | ||
|
|
dbe6f2714f | ||
|
|
25220a46ee | ||
|
|
ff3a0fffea | ||
|
|
c9f71ec6f2 | ||
|
|
6bc8aabb6b | ||
|
|
f577e5446a | ||
|
|
d18f891e8f | ||
|
|
ca6514fa45 | ||
|
|
14043d88e5 | ||
|
|
73234133b1 | ||
|
|
5436aa10f0 | ||
|
|
87ffcbef1c | ||
|
|
4d297385a7 | ||
|
|
6e5ebd50ba | ||
|
|
9020530a74 | ||
|
|
44f19b13a7 | ||
|
|
a30aa5d873 | ||
|
|
4d611cc0b9 | ||
|
|
918f0844eb | ||
|
|
4f56595003 | ||
|
|
42dcd45392 | ||
|
|
f43cc09b6f | ||
|
|
8f4eb8596d | ||
|
|
5459914156 | ||
|
|
ecce77bbc6 | ||
|
|
dccfd3ce08 | ||
|
|
c29457d341 | ||
|
|
dfdee167a4 | ||
|
|
ab17ab70c5 | ||
|
|
e79b2a3ad3 | ||
|
|
53c50afca0 | ||
|
|
43862557f4 | ||
|
|
204da34651 | ||
|
|
f92fd358c8 | ||
|
|
407b8b213c | ||
|
|
9a14dba0f8 | ||
|
|
94e4b00226 | ||
|
|
19aaf067d8 | ||
|
|
9de2a69c26 | ||
|
|
059644a27d | ||
|
|
ac12db1738 | ||
|
|
bcf4485331 | ||
|
|
8493f9ca2c | ||
|
|
e89a742b24 | ||
|
|
020e02fb50 | ||
|
|
5bbd10dfbb | ||
|
|
eb102da367 | ||
|
|
2dd7d8d9ba | ||
|
|
f6690b2bee | ||
|
|
5f0bd1852d | ||
|
|
ff592f0bd3 | ||
|
|
078a713302 | ||
|
|
8fa6f80acd | ||
|
|
a0576418aa | ||
|
|
b1b892bbc8 | ||
|
|
24fbcd2859 | ||
|
|
7d421a482d | ||
|
|
14438ac5b3 | ||
|
|
93fb466075 | ||
|
|
0f5ad7e851 | ||
|
|
a61adf78b6 | ||
|
|
a1f8ee4a0e | ||
|
|
9ed0f74820 | ||
|
|
b124d8b4c4 | ||
|
|
7b40392bc5 | ||
|
|
3138c7b3c4 | ||
|
|
84e854dc39 | ||
|
|
40c592aad4 | ||
|
|
f8b0d027d7 | ||
|
|
3d90acdfa4 | ||
|
|
850850bc29 | ||
|
|
d2e02453d8 | ||
|
|
7675fec424 | ||
|
|
db6737b157 | ||
|
|
90897eccd3 | ||
|
|
2dca199f32 | ||
|
|
e63ea63cc5 | ||
|
|
cad0fa589b | ||
|
|
2072d960c8 | ||
|
|
2d631fce66 | ||
|
|
cddcce8cbf | ||
|
|
d39df3d883 | ||
|
|
90e5806813 | ||
|
|
de458b2779 | ||
|
|
ff6486ae59 | ||
|
|
20607547df | ||
|
|
661f297bd5 | ||
|
|
90ac2f56d8 | ||
|
|
3eb4b933a7 | ||
|
|
d5666fcbd0 | ||
|
|
9561e71299 | ||
|
|
55f4d705b0 | ||
|
|
246e3e2bef | ||
|
|
2ba61df85b | ||
|
|
30be1aab32 | ||
|
|
865dbbf17a | ||
|
|
fa5c799905 | ||
|
|
dc094bad11 | ||
|
|
0cc7d20b0b | ||
|
|
527e775952 | ||
|
|
43dc22c766 | ||
|
|
92dca74763 | ||
|
|
9738d0e549 | ||
|
|
3ed6695f28 | ||
|
|
bbbb10e846 | ||
|
|
6586d30961 | ||
|
|
4a86179c1b | ||
|
|
dbae78d2f9 | ||
|
|
341bdaaf44 | ||
|
|
0f36ae74ad | ||
|
|
e3abd8fb01 | ||
|
|
bba0ba7f19 | ||
|
|
3a7efd879f | ||
|
|
33e3fe12e8 | ||
|
|
f12c185b18 | ||
|
|
ca3b717395 | ||
|
|
b3a277a734 | ||
|
|
5b684e42e6 | ||
|
|
e7609f6e43 | ||
|
|
0a782b24a6 | ||
|
|
4ccc28a117 | ||
|
|
73d438de27 | ||
|
|
9f819527f5 | ||
|
|
d584f82516 | ||
|
|
bf5cda35e4 | ||
|
|
2ee8dd1e37 | ||
|
|
cea0ffd382 | ||
|
|
2e7e4299e7 | ||
|
|
a515ab8bdf | ||
|
|
dd9e41f532 | ||
|
|
9a01efe126 | ||
|
|
29fa7f6e7b | ||
|
|
de5fb29e04 | ||
|
|
119f0e464f | ||
|
|
b173ae4dc3 | ||
|
|
e94afc8a39 | ||
|
|
8946523053 | ||
|
|
a8217cbe40 | ||
|
|
a1c972c4c6 | ||
|
|
18401c4fc3 | ||
|
|
f86e2fd957 | ||
|
|
5c7fa18e4d | ||
|
|
1db58cb862 | ||
|
|
7dacfb64aa | ||
|
|
f6dc3cc324 | ||
|
|
f62cd7649b | ||
|
|
fd581083ec | ||
|
|
7d330e5804 | ||
|
|
e72a8f7c03 | ||
|
|
94cdb89f5c | ||
|
|
059e9a6844 | ||
|
|
dc412bd1b7 | ||
|
|
9d53ee1094 | ||
|
|
a8977eebcc | ||
|
|
060bd0261f | ||
|
|
989404d4f0 | ||
|
|
da23cbf28d | ||
|
|
43aba315e8 | ||
|
|
7ee51141d6 | ||
|
|
b215bc6a39 | ||
|
|
ab32251643 | ||
|
|
c37701ef1e | ||
|
|
7c97129dcf | ||
|
|
e510f1de50 | ||
|
|
0394e4dff7 | ||
|
|
419cfc1f39 | ||
|
|
6b29ad054b | ||
|
|
1456639ba9 | ||
|
|
4b9586ad8d | ||
|
|
1db51b7cfb | ||
|
|
5742c8eb23 | ||
|
|
71778ea3d8 | ||
|
|
dbbf70cd35 | ||
|
|
f64293c92d | ||
|
|
fe49c460eb | ||
|
|
689d9af022 | ||
|
|
9a0c6b2ed0 | ||
|
|
046f1019f5 | ||
|
|
543450d859 | ||
|
|
eafc717dd8 | ||
|
|
7b64fa487b | ||
|
|
d890ee248c | ||
|
|
d3d84db23d | ||
|
|
db9114986f | ||
|
|
815150de4e | ||
|
|
1d797ebe80 | ||
|
|
e65f8e490e | ||
|
|
e91a82c775 | ||
|
|
ef751a2e11 | ||
|
|
9ce48be89b | ||
|
|
a0b092c544 | ||
|
|
dce0cfafae | ||
|
|
79dd082e00 | ||
|
|
9cc764188f | ||
|
|
63874041b4 | ||
|
|
bfffb5114c | ||
|
|
f8de4ffc2d | ||
|
|
d9b1dfef76 | ||
|
|
a56e760d36 | ||
|
|
69b1e15d4c | ||
|
|
91ce89acef | ||
|
|
dcb02999b4 | ||
|
|
6782adc545 | ||
|
|
10d42421d0 | ||
|
|
9df831ea6f | ||
|
|
82c4243049 | ||
|
|
a34135a946 | ||
|
|
e8300a0288 | ||
|
|
681227e5fa | ||
|
|
3c46d4eb96 | ||
|
|
37c43d085f | ||
|
|
6388f1ba1b | ||
|
|
9a0504714c | ||
|
|
03cc059bf7 | ||
|
|
1b8f1ec467 | ||
|
|
d6b29340c6 | ||
|
|
904a852f1b | ||
|
|
ae252e0649 | ||
|
|
c02028f6f8 | ||
|
|
2905c2920c | ||
|
|
ce66959034 | ||
|
|
73d0c5e92d | ||
|
|
5dcf2ffc76 | ||
|
|
d21d7afe9a | ||
|
|
1e0676562d | ||
|
|
ee2361d012 | ||
|
|
66c4769d78 | ||
|
|
0585181b28 | ||
|
|
eb2208ce1e | ||
|
|
1968c3e1fc | ||
|
|
2b1a2340ff | ||
|
|
0703a6885f | ||
|
|
76c197cd7b | ||
|
|
0e194d7574 | ||
|
|
488e3a1121 | ||
|
|
7cb02e3cb3 | ||
|
|
9586e7f87e | ||
|
|
426e452ab5 | ||
|
|
7e7d83564f | ||
|
|
e171e802c1 | ||
|
|
dfb713eb79 | ||
|
|
b28001def5 | ||
|
|
c67194d5e6 | ||
|
|
5ce68f2d8b | ||
|
|
20b17cbdbe | ||
|
|
61697ea941 | ||
|
|
edf5c132ce | ||
|
|
dca2d7db48 | ||
|
|
d885390cab | ||
|
|
8e54b0e742 | ||
|
|
2401aa7ab5 | ||
|
|
6c0ca4ff08 | ||
|
|
366de65e8b | ||
|
|
efe493d179 | ||
|
|
a4374b61d7 | ||
|
|
3b5fa24950 | ||
|
|
7411807922 | ||
|
|
efcdb5a300 | ||
|
|
0c06d78551 | ||
|
|
573b4c9377 | ||
|
|
f9bbe4b0d6 | ||
|
|
f192e1cf27 | ||
|
|
07383f7c4c | ||
|
|
4498547824 | ||
|
|
bd7da33e77 | ||
|
|
7dc6573513 | ||
|
|
59c95f2ccf | ||
|
|
ce2841b05e | ||
|
|
bdc44a4395 | ||
|
|
f5c119e27c | ||
|
|
785c8ecdd8 | ||
|
|
58275457de | ||
|
|
fb7d3115ce | ||
|
|
c96d9c2bf7 | ||
|
|
0dff0c488b | ||
|
|
a740bc262e | ||
|
|
8ab986f2e9 | ||
|
|
1bbe8cd17c | ||
|
|
2169e15742 | ||
|
|
fa590b5eb1 | ||
|
|
196ab76e30 | ||
|
|
8329a0f653 | ||
|
|
8b1c9e0b7d | ||
|
|
b9d18fd38c | ||
|
|
30656d38c4 | ||
|
|
b9e8528029 | ||
|
|
cb082f1e31 | ||
|
|
f270e4d9cd | ||
|
|
ba5b48cb2b | ||
|
|
407616c515 | ||
|
|
29ae8f5446 | ||
|
|
2cc8166583 | ||
|
|
85bd53b0c6 | ||
|
|
7d04f015f4 | ||
|
|
51fd3f7647 | ||
|
|
ba362ede36 | ||
|
|
193336f5f3 | ||
|
|
5cf63692a8 | ||
|
|
4612dbbc59 | ||
|
|
84a82990fa | ||
|
|
04af457676 | ||
|
|
708c43edbf | ||
|
|
93ee8f7c7b | ||
|
|
ebe7d72789 | ||
|
|
6fa5e16d5f | ||
|
|
78f652dd43 | ||
|
|
10724a5161 | ||
|
|
6c40e8214d | ||
|
|
a73bd2fa3d | ||
|
|
9ee851dabc | ||
|
|
d9fba35f64 | ||
|
|
60c62c4713 | ||
|
|
832db1d89d | ||
|
|
91d34b404d | ||
|
|
a744fea295 | ||
|
|
17951d9c7f | ||
|
|
ff4b5fad66 | ||
|
|
923bc0a567 | ||
|
|
13b729134a | ||
|
|
517d927391 | ||
|
|
fa9669894d | ||
|
|
27ccd4859b | ||
|
|
850409ec62 | ||
|
|
800782a3b4 | ||
|
|
59a34ae91e | ||
|
|
5894806df5 | ||
|
|
56f225be68 | ||
|
|
c0fe6186e9 | ||
|
|
0e01478dbf | ||
|
|
668fb1e202 | ||
|
|
ceb2002374 | ||
|
|
3975b3bf6d | ||
|
|
1b8d38c417 | ||
|
|
60a94f6022 | ||
|
|
824e0f18f2 | ||
|
|
08e8e414fc | ||
|
|
0327cd73f1 | ||
|
|
46b65e835c | ||
|
|
0e47e160d6 | ||
|
|
26068c3033 | ||
|
|
401b0db85f | ||
|
|
0f06c93efc | ||
|
|
d614402a07 | ||
|
|
81f2969375 | ||
|
|
ced97449f4 | ||
|
|
399d03239d | ||
|
|
5e85516728 | ||
|
|
ca2b7887f0 | ||
|
|
3c3662dc0b | ||
|
|
74566f1f4e | ||
|
|
dddd09efe7 | ||
|
|
f313de53c0 | ||
|
|
3ae582ef97 | ||
|
|
23cab1ef8e | ||
|
|
25380e0a5c | ||
|
|
e812f41ae7 | ||
|
|
6fe794baa9 | ||
|
|
8733c63c58 | ||
|
|
54712f0c66 | ||
|
|
4f82662da8 | ||
|
|
056d50b6ba | ||
|
|
3831f4b5e2 | ||
|
|
858b8731f8 | ||
|
|
142fd19dec | ||
|
|
9f89b3528b | ||
|
|
88446acb39 | ||
|
|
d25a2774d3 | ||
|
|
07d2b0aaf3 | ||
|
|
d68ee9c507 | ||
|
|
47b9b226e1 | ||
|
|
db1479f6b9 | ||
|
|
b42c2ad489 | ||
|
|
ea7b290850 | ||
|
|
d8415d85ef | ||
|
|
4e85678560 | ||
|
|
5e335b97fa | ||
|
|
fd5a969434 | ||
|
|
b58b8e2051 | ||
|
|
38eda65143 | ||
|
|
ddf8ce3ac5 | ||
|
|
8de0472048 | ||
|
|
75931881f7 | ||
|
|
6d24ddc8e6 | ||
|
|
54f5af72f2 | ||
|
|
4d3cdaa26f | ||
|
|
e29cb17712 | ||
|
|
eb22d38770 | ||
|
|
244d98d81f | ||
|
|
e2c57c0973 | ||
|
|
6ec968cd85 | ||
|
|
0a4270970d | ||
|
|
50f978bc5c | ||
|
|
2543dd0bae | ||
|
|
2d9cdcaef6 | ||
|
|
ed612574bc | ||
|
|
20b28e3d06 | ||
|
|
bfd667de00 | ||
|
|
a551612b89 | ||
|
|
7e4b909278 | ||
|
|
8448148f97 | ||
|
|
dc3efbc739 | ||
|
|
2fd5ed045d | ||
|
|
5c63c5fe1c | ||
|
|
8f1ecbb12b | ||
|
|
8247399af8 | ||
|
|
a89106211f | ||
|
|
e1202c6839 | ||
|
|
939e461c08 | ||
|
|
653329be09 | ||
|
|
a2223fc6f9 | ||
|
|
2f02b03d6a | ||
|
|
d851a4fc8d | ||
|
|
c54d24a362 | ||
|
|
c2797969d4 | ||
|
|
f34dc83907 | ||
|
|
85ad236693 | ||
|
|
97cd2130b4 | ||
|
|
3bf91bdde6 | ||
|
|
8304196533 | ||
|
|
d1fbe5c26d | ||
|
|
b43a313793 | ||
|
|
20b89b98ed | ||
|
|
6be0b30d04 | ||
|
|
8f4909ad9b | ||
|
|
be778e451e | ||
|
|
5b1fac6734 | ||
|
|
d5b96c0051 | ||
|
|
eaa5078521 | ||
|
|
1ac0eb83cf | ||
|
|
f1907012f7 | ||
|
|
dc7f692f38 | ||
|
|
8dd5faf9c9 | ||
|
|
80963222d1 | ||
|
|
2a3456d0e2 | ||
|
|
01de70351b | ||
|
|
d002a9311c | ||
|
|
1173dca6c5 | ||
|
|
a9a9124547 | ||
|
|
aa616e557a | ||
|
|
69c0aba875 | ||
|
|
7fb93a4f1d | ||
|
|
390b041cb2 | ||
|
|
0dfbfffe76 | ||
|
|
ae5bb38c92 | ||
|
|
ccb5e6d6a5 | ||
|
|
e97b814089 | ||
|
|
6cb14c2568 | ||
|
|
b142ff53b5 | ||
|
|
6326b1c185 | ||
|
|
20b6d2c27b | ||
|
|
bacacdcb45 | ||
|
|
101521833d | ||
|
|
ff11d7156d | ||
|
|
a797a595c8 | ||
|
|
874757a79a | ||
|
|
08d7287099 | ||
|
|
487e717924 | ||
|
|
730d9ebb70 | ||
|
|
4793cf77da | ||
|
|
2c3069b5a2 | ||
|
|
176c5f1871 | ||
|
|
38e47bbc5c | ||
|
|
f7a0f528d2 | ||
|
|
6482c8e05a | ||
|
|
881905cf21 | ||
|
|
e531f7300e | ||
|
|
c78411b7ee | ||
|
|
017deaaad5 | ||
|
|
433508b70b | ||
|
|
539c66779f | ||
|
|
83dfd3c739 | ||
|
|
3c17326928 | ||
|
|
ac5e585387 | ||
|
|
499f90b7a2 | ||
|
|
b0b16feb1f | ||
|
|
7d13720dad | ||
|
|
82dcccb5b3 | ||
|
|
a3efd3dfe6 | ||
|
|
c870edd9f2 | ||
|
|
5de048a81a | ||
|
|
00dcf5c2c6 | ||
|
|
e89c39d2e1 | ||
|
|
ccd3e87403 | ||
|
|
6e2f668e72 | ||
|
|
4896e47b41 | ||
|
|
0b72fee8a3 | ||
|
|
56e5bfa234 | ||
|
|
d9deb6149c | ||
|
|
3c9bf4f9c8 | ||
|
|
e70f89b67c | ||
|
|
4521cc88fa | ||
|
|
5cd3a38fc1 | ||
|
|
4067aca8fc | ||
|
|
7fdc63c8a5 | ||
|
|
c29cdcb757 | ||
|
|
f4443ea97b | ||
|
|
a83e193133 | ||
|
|
b985642e5e | ||
|
|
d49e20cfa4 | ||
|
|
af34fda5aa | ||
|
|
7f7d2bf1dd | ||
|
|
8dbbfd5c07 | ||
|
|
02ae4d8460 | ||
|
|
e34adad368 | ||
|
|
55ec69ec79 | ||
|
|
a9cf83aec2 | ||
|
|
35fc6ee211 | ||
|
|
e61c2f1554 | ||
|
|
e6af43b060 | ||
|
|
e11fe9db8b | ||
|
|
14ba4e6894 | ||
|
|
18e2d127be | ||
|
|
b77d191754 | ||
|
|
b04f644242 | ||
|
|
570677fe15 | ||
|
|
a5848f4467 | ||
|
|
b956760d1e | ||
|
|
3118aa7b35 | ||
|
|
9b08ace35c | ||
|
|
39d1a7b3da | ||
|
|
f0216cb3da | ||
|
|
03a45a1185 | ||
|
|
f580d49a56 | ||
|
|
284829fc65 | ||
|
|
29e5316439 | ||
|
|
eedd169f15 | ||
|
|
d0227a7f99 | ||
|
|
8f6ba8553a | ||
|
|
0c9d134429 | ||
|
|
e1aa757a3d | ||
|
|
69d3db30cd | ||
|
|
3906cf9cc0 | ||
|
|
779680195d | ||
|
|
25d4e0e524 | ||
|
|
06b7f2d677 | ||
|
|
fdf58e82a3 | ||
|
|
025b6db6ef | ||
|
|
c5a62a46a8 | ||
|
|
f99bc762a9 | ||
|
|
2b0fdec1d0 | ||
|
|
cfaf795bff | ||
|
|
247ed0e597 | ||
|
|
4a0f58862f | ||
|
|
fe8ab69652 | ||
|
|
82e4dd2cfd | ||
|
|
08c6c7f725 | ||
|
|
9388a7c825 | ||
|
|
55c9783414 | ||
|
|
7cebee11a5 | ||
|
|
f927cb41e0 | ||
|
|
f53c7f5f78 | ||
|
|
8f3aec9ec1 | ||
|
|
1b7bdd2e57 | ||
|
|
4e04d7e28f | ||
|
|
99de0acc8f | ||
|
|
0bba96c1b7 | ||
|
|
84e473c5cb | ||
|
|
f91faf28d8 | ||
|
|
52d3318a9c | ||
|
|
cec608b8d0 | ||
|
|
1c559ff538 | ||
|
|
23b067ee95 | ||
|
|
009af245dc | ||
|
|
02f7ff8979 | ||
|
|
053726519c | ||
|
|
e031ac8176 | ||
|
|
ef96037555 | ||
|
|
3c1edcef4e | ||
|
|
6481842482 | ||
|
|
048737d9dd | ||
|
|
6b040f9bb8 | ||
|
|
9a57f1aca5 | ||
|
|
3f99525715 | ||
|
|
0b5067b6e8 | ||
|
|
124a9cec6c | ||
|
|
2c21bd0ffb | ||
|
|
e8cf3a1828 | ||
|
|
faa322f4a7 | ||
|
|
cb1debc8f4 | ||
|
|
0264571cce | ||
|
|
35569a69b7 | ||
|
|
9a9f5ec268 | ||
|
|
2a69451834 | ||
|
|
5f560c2266 | ||
|
|
10be0a93a4 | ||
|
|
907825f53b | ||
|
|
755d2476e0 | ||
|
|
101727b5da | ||
|
|
2de0a4d164 | ||
|
|
32d276cb7e | ||
|
|
95cf25d531 | ||
|
|
7653f50f9d | ||
|
|
599407ff14 | ||
|
|
b4d57200a6 | ||
|
|
4e15c2ddd4 | ||
|
|
1a8b0bded3 | ||
|
|
9650bacca5 | ||
|
|
ba29d1cfd9 | ||
|
|
51a0a9e70a | ||
|
|
70a82fbc45 | ||
|
|
144f8944e1 | ||
|
|
8d3c4ff443 | ||
|
|
31113133fc | ||
|
|
2bf72bca6e | ||
|
|
1b01336514 | ||
|
|
d52deee44e | ||
|
|
d4c25caeef | ||
|
|
614e98d32a | ||
|
|
00360e55d2 | ||
|
|
8597956aeb | ||
|
|
8c898b79b8 | ||
|
|
544cfc5044 | ||
|
|
e3f0de8f84 | ||
|
|
b160281cfe | ||
|
|
321a44d0e9 | ||
|
|
3bf52a1237 | ||
|
|
466297712d | ||
|
|
43f60a5927 | ||
|
|
2d47b99618 | ||
|
|
cb66655c18 | ||
|
|
981dcade2b | ||
|
|
fc15e64c54 | ||
|
|
fdcb9bd7a4 | ||
|
|
e6f4661077 | ||
|
|
912f28c06a | ||
|
|
561d0fec79 | ||
|
|
eff5e56219 | ||
|
|
e594c6f11e | ||
|
|
c28e8a700d | ||
|
|
617ff419b9 | ||
|
|
d9b5d19e8f | ||
|
|
2d26abac38 | ||
|
|
2d31093d2d | ||
|
|
7f30db66b6 | ||
|
|
a05c75f838 | ||
|
|
efae93345a | ||
|
|
e236a107c9 | ||
|
|
b033ab0bba | ||
|
|
e80893e4a0 | ||
|
|
f9aa7ece1c | ||
|
|
1000b73ecb | ||
|
|
4105d54582 | ||
|
|
270bd3cb73 | ||
|
|
b581d722c3 | ||
|
|
bc84022d52 | ||
|
|
508b1c6c49 | ||
|
|
d66ce972f0 | ||
|
|
adc7ea417e | ||
|
|
3a5bffdb0c | ||
|
|
2c27c2754f | ||
|
|
9877a8d15f | ||
|
|
50d9003ea8 | ||
|
|
172600a94d | ||
|
|
1b8bc63b07 | ||
|
|
bb12706151 | ||
|
|
18f241f036 | ||
|
|
65aa1b8d89 | ||
|
|
b91517812a | ||
|
|
66c9b2ae25 | ||
|
|
9739dffd71 | ||
|
|
8cd5948375 | ||
|
|
22d9c09403 | ||
|
|
336b1f309a | ||
|
|
86fd37b80b | ||
|
|
54fbb78bea | ||
|
|
b6a2d9ed73 | ||
|
|
118d2333ec | ||
|
|
3ee4535ebd | ||
|
|
8fb779b7ec | ||
|
|
295bfbe8a3 | ||
|
|
ccc2629e47 | ||
|
|
ac3bec4332 | ||
|
|
03551d6968 | ||
|
|
f6c9bdb5e2 | ||
|
|
8cc0744698 | ||
|
|
5f7bcb4d69 | ||
|
|
88538a500f | ||
|
|
871065bdca | ||
|
|
26c2845d4f | ||
|
|
bee2be36cf | ||
|
|
6897f36e03 | ||
|
|
117ff56bbc | ||
|
|
d3ff88f934 | ||
|
|
93b5eb0b1e | ||
|
|
712a907f34 | ||
|
|
e13ae86a05 | ||
|
|
41e048d382 | ||
|
|
87d236828f | ||
|
|
8d73b8e132 | ||
|
|
a1400a0318 | ||
|
|
65e88caed4 | ||
|
|
14413dcdd7 | ||
|
|
5c90cf5a2c | ||
|
|
d2473fe3a6 | ||
|
|
64725a4c41 | ||
|
|
4330d95866 | ||
|
|
6897d1d31a | ||
|
|
63bb900889 | ||
|
|
c1ca1f0a99 | ||
|
|
98762a6c77 | ||
|
|
fd98f9b7f9 | ||
|
|
946a1e9cb4 | ||
|
|
244b52a8b3 | ||
|
|
e82103ba94 | ||
|
|
cf792b7ed5 | ||
|
|
31b76892cd | ||
|
|
ba942bff3d | ||
|
|
c5a351f6ca | ||
|
|
27e9a56d04 | ||
|
|
8b00ab6bf5 | ||
|
|
86cbc8a613 | ||
|
|
69c17e9b4c | ||
|
|
01d91402fc | ||
|
|
281a8c7e66 | ||
|
|
ef758638af | ||
|
|
0b2331b0e6 | ||
|
|
8d114e42f1 | ||
|
|
89fef80d05 | ||
|
|
c9a0058042 | ||
|
|
740884847f | ||
|
|
5a68bc4b79 | ||
|
|
f3006fd8e2 | ||
|
|
0b616164c4 | ||
|
|
3cfaa8ad99 | ||
|
|
187256cdb7 | ||
|
|
adc2d95b1f | ||
|
|
2973214976 | ||
|
|
f8418f689f | ||
|
|
8d03b23eb4 | ||
|
|
d1dd1c7530 | ||
|
|
9aeed6ac31 | ||
|
|
fcc8a8f9ce | ||
|
|
67e5ae9ee0 | ||
|
|
2b49f70ae3 | ||
|
|
7813483a08 | ||
|
|
045b303deb | ||
|
|
57af402bcf | ||
|
|
e4b7c105b1 | ||
|
|
3b5d273409 | ||
|
|
07c29999c6 | ||
|
|
9dc7632e68 | ||
|
|
442ffdcb37 | ||
|
|
f02ba82471 | ||
|
|
f628a4aa65 | ||
|
|
20b80bced6 | ||
|
|
801193edde | ||
|
|
8eaf76734e | ||
|
|
9a1dcd3cda | ||
|
|
553cec6d60 | ||
|
|
c333fec6c4 | ||
|
|
7e9efd9c30 | ||
|
|
8e7f055b59 | ||
|
|
ece779c88e | ||
|
|
c1da970c47 | ||
|
|
3505047c9b | ||
|
|
1338af9720 | ||
|
|
e0dd4d3b4f | ||
|
|
1458184500 | ||
|
|
52fe47c3ee | ||
|
|
1a81f24c45 | ||
|
|
63210a42b0 | ||
|
|
09900a3457 | ||
|
|
7071be93f7 | ||
|
|
96a7d7d0d2 | ||
|
|
2974e33751 | ||
|
|
854205f86f | ||
|
|
74e4f2aa4a | ||
|
|
eadb5aebb7 | ||
|
|
ae1de32654 | ||
|
|
b3d70f1f0d | ||
|
|
b7651abf4a | ||
|
|
7005a2fbe4 | ||
|
|
e9dd24e6c8 | ||
|
|
ed034bdec7 | ||
|
|
bbd8c0c752 | ||
|
|
1bb807e302 | ||
|
|
114185ae1e | ||
|
|
b91791c747 | ||
|
|
e51fdd8c56 | ||
|
|
8c695fd4d9 | ||
|
|
7823c28540 | ||
|
|
7aed102941 | ||
|
|
15e8ac7640 | ||
|
|
5e5d78e595 | ||
|
|
5bfb8aa8ac | ||
|
|
9345f7bb23 | ||
|
|
261da41e85 | ||
|
|
ee8c7ba84e | ||
|
|
64e3718122 | ||
|
|
c0e715717b | ||
|
|
4a4e38fdd0 | ||
|
|
05bc65aba9 | ||
|
|
ac27e07225 | ||
|
|
e9d8e9ba78 | ||
|
|
53a521ecd2 | ||
|
|
ff12e5b088 | ||
|
|
2fada1da8d | ||
|
|
c003ddeeac | ||
|
|
df7f893302 | ||
|
|
21c41550f8 | ||
|
|
e38a73b590 | ||
|
|
8e6755482a | ||
|
|
c96b918fad | ||
|
|
ecb6a10c7f | ||
|
|
790a461622 | ||
|
|
ec94798cc2 | ||
|
|
b94b1e3df1 | ||
|
|
ef75e00184 | ||
|
|
5c03d27538 | ||
|
|
ccb1aa9ef0 | ||
|
|
54e2951e4b | ||
|
|
e8c6f29577 | ||
|
|
e0880f1932 | ||
|
|
465c450b6b | ||
|
|
0d4088ff7a | ||
|
|
7e3b9ccc2d | ||
|
|
d96d1f2ee9 | ||
|
|
ed76e62d56 | ||
|
|
43e4d8a914 | ||
|
|
714a77f451 | ||
|
|
3f8651f586 | ||
|
|
a25592ebfd | ||
|
|
4a131b63d8 | ||
|
|
2e8ee84485 | ||
|
|
fb1c545a98 | ||
|
|
0a03261dbb | ||
|
|
1c3fc52b6a | ||
|
|
b091b892e9 | ||
|
|
74e84dc381 | ||
|
|
1d4d18566c | ||
|
|
4c083a127f | ||
|
|
1df11a1f81 | ||
|
|
41698bfddd | ||
|
|
39d143cebc | ||
|
|
4576bb28ac | ||
|
|
998efa11e3 | ||
|
|
66870cdd05 | ||
|
|
42ead5b9eb | ||
|
|
852e12c903 | ||
|
|
058aabb666 | ||
|
|
34ee393224 | ||
|
|
816660f80d | ||
|
|
1e1a16a4f4 | ||
|
|
bf2eed7cf5 | ||
|
|
d13201fd47 | ||
|
|
5debe17b08 | ||
|
|
aa34d92668 | ||
|
|
279ff2c765 | ||
|
|
1ebb4aa091 | ||
|
|
176705e703 | ||
|
|
82afad3b67 | ||
|
|
5d43733add | ||
|
|
e07b34f9cb | ||
|
|
a1203b2c4a | ||
|
|
f0ce6277ac | ||
|
|
95bd867cee | ||
|
|
ac63687c33 | ||
|
|
924e063168 | ||
|
|
8a52feaf6d | ||
|
|
773358e538 | ||
|
|
44fc9be4a2 | ||
|
|
697be27d4f | ||
|
|
fb23e5a663 | ||
|
|
12631ffee6 | ||
|
|
cbce073227 | ||
|
|
4103afc6cf | ||
|
|
c2ddc8cd87 | ||
|
|
52a5f3b0a4 | ||
|
|
91d80f49f4 | ||
|
|
e0b1bc7cd0 | ||
|
|
438d4f6e38 | ||
|
|
473fa088ce | ||
|
|
e52b9d62f7 | ||
|
|
c7fb979fe2 | ||
|
|
198e7adc47 | ||
|
|
4cf79222f0 | ||
|
|
94a9597963 | ||
|
|
469649c0c4 | ||
|
|
bd08c5ca9d | ||
|
|
1296b24960 | ||
|
|
87b8ac38f2 | ||
|
|
51dca51014 | ||
|
|
07c448ba16 | ||
|
|
0033edbeda | ||
|
|
d7e701d7a5 | ||
|
|
63bd305250 | ||
|
|
dc789addb9 | ||
|
|
6b1c0a0eda | ||
|
|
a3f36bd7a4 | ||
|
|
1258e24ee0 | ||
|
|
f9832a51cf | ||
|
|
018a3d8c91 | ||
|
|
ec1a4579e1 | ||
|
|
bfde06f0d6 | ||
|
|
b8c207ac2a | ||
|
|
9b2e54c001 | ||
|
|
62585848bd | ||
|
|
d47acc16a9 | ||
|
|
a34e97af52 | ||
|
|
319b587c1b | ||
|
|
07f0f06767 | ||
|
|
ac3513ac8a | ||
|
|
5760520ca2 | ||
|
|
236f456097 | ||
|
|
2965e0fc28 | ||
|
|
0ccdbde2f7 | ||
|
|
b2659d6c38 | ||
|
|
52b300650c | ||
|
|
0b05bde1e3 | ||
|
|
b72b528cb3 | ||
|
|
ab98c1df7d | ||
|
|
bc63cdd52a | ||
|
|
ada5aef26f | ||
|
|
df1050668f | ||
|
|
4dc13f9417 | ||
|
|
d71da723a0 | ||
|
|
c5ce102f4d | ||
|
|
3d291444bc | ||
|
|
270e25083a | ||
|
|
b6f55309ae | ||
|
|
4afeaeb170 | ||
|
|
932dde8177 | ||
|
|
41b4ae9aae | ||
|
|
9d66d36a78 | ||
|
|
a4133a07a8 | ||
|
|
f73d139aa7 | ||
|
|
d6ca320c61 | ||
|
|
bbf645bc6b | ||
|
|
ea547dfd4d | ||
|
|
d990f245d5 | ||
|
|
bd0bad4254 | ||
|
|
a0c2c41203 | ||
|
|
74606f6b04 | ||
|
|
cca8f84f61 | ||
|
|
b3aaf2e0e4 | ||
|
|
5bf954c503 | ||
|
|
c66a8c20d2 | ||
|
|
d664f8abcd | ||
|
|
403b1f3c39 | ||
|
|
38697f0835 | ||
|
|
0b5beddd51 | ||
|
|
a6c3a642f8 | ||
|
|
5e8d761a41 | ||
|
|
b4fd663329 | ||
|
|
124c1632af | ||
|
|
3867287c0f | ||
|
|
c1c0424af9 | ||
|
|
cd9d70baee | ||
|
|
13aec8f621 | ||
|
|
7b5e7afd6d | ||
|
|
a586be2c41 | ||
|
|
4630623d35 | ||
|
|
71a2e68551 | ||
|
|
ef71e037f2 | ||
|
|
df30218f55 | ||
|
|
e8a84ad571 | ||
|
|
43dfbed157 | ||
|
|
8b8605b773 | ||
|
|
9ef27b2699 | ||
|
|
faad9e0d2c | ||
|
|
6a9ba00aca | ||
|
|
5329138d0f | ||
|
|
ed0109af94 | ||
|
|
92556d5cb2 | ||
|
|
1a3308017a | ||
|
|
64e8dd35c2 | ||
|
|
bf5d8b5472 | ||
|
|
f8beba2815 | ||
|
|
8c325d8264 | ||
|
|
9aec30e0dc | ||
|
|
5049b7885a | ||
|
|
3eead9da4e | ||
|
|
9a206ff338 | ||
|
|
4f487f3009 | ||
|
|
d7a55e4c7d | ||
|
|
cc654b56a1 | ||
|
|
66ac3ee63c | ||
|
|
e421a32475 | ||
|
|
1acb0e9e95 | ||
|
|
ab2d284dee | ||
|
|
f72c5aa59c | ||
|
|
910dc4fb26 | ||
|
|
4bf54c721d | ||
|
|
b482964373 | ||
|
|
01017efab6 | ||
|
|
49558ca39d | ||
|
|
758dbb689a | ||
|
|
536f9f4409 | ||
|
|
c6cab341ed | ||
|
|
792fdad4be | ||
|
|
bd097888a3 | ||
|
|
eae90194cb | ||
|
|
0a717bb9be | ||
|
|
dc71046018 | ||
|
|
881572cba5 | ||
|
|
4a41c57e93 | ||
|
|
084f45393e | ||
|
|
90a1a5f058 | ||
|
|
e0486aff29 | ||
|
|
ce165f4234 | ||
|
|
4d73f954f8 | ||
|
|
445625ba1e | ||
|
|
e76e610b97 | ||
|
|
8c50ddef2f | ||
|
|
d11c608bd0 | ||
|
|
9aac61e350 | ||
|
|
6436d6fa2a | ||
|
|
8f68266585 | ||
|
|
38376b3978 | ||
|
|
7d5b0be5ca | ||
|
|
98bc1fb01a | ||
|
|
aa11704caa | ||
|
|
bc701fecb3 | ||
|
|
e8462e8291 | ||
|
|
d74c6a2e21 | ||
|
|
bddd94aec0 | ||
|
|
665d07cd56 | ||
|
|
7375ad8a5d | ||
|
|
581461f0e3 | ||
|
|
3dc3da5932 | ||
|
|
0464601dc5 | ||
|
|
3f31bf85fb | ||
|
|
7c4429da46 | ||
|
|
b73efba916 | ||
|
|
58cbcee5ba | ||
|
|
ab8b71b152 | ||
|
|
ca71a997dd | ||
|
|
ce6ddd2f91 | ||
|
|
c938f8e731 | ||
|
|
7df7703c50 | ||
|
|
33bc3557e5 | ||
|
|
cf2bc158a0 | ||
|
|
e8d0fe07f4 | ||
|
|
a4fa7cb5ee | ||
|
|
6ea52c93fe | ||
|
|
b28887e91c | ||
|
|
263b906aec | ||
|
|
acef14aadc | ||
|
|
055ebac496 | ||
|
|
285370fe35 | ||
|
|
23af3953e6 | ||
|
|
83665d0992 | ||
|
|
c6dcb9db2b | ||
|
|
b3205d43db | ||
|
|
4feefd404b | ||
|
|
b202642793 | ||
|
|
ea3e91483a | ||
|
|
4ae6d9030a | ||
|
|
c97353cb3d | ||
|
|
71b2f2fa26 | ||
|
|
3c8915b002 | ||
|
|
a54a9beb87 | ||
|
|
0ad2dcb234 | ||
|
|
2b34c68057 | ||
|
|
f7a739a023 | ||
|
|
3d38b029a1 | ||
|
|
9ffad0437a | ||
|
|
844c083ee4 | ||
|
|
813b20a55c | ||
|
|
a8277f5e13 | ||
|
|
922c7e148c | ||
|
|
6f421e688a | ||
|
|
e5db8ff8b3 | ||
|
|
75d6ea6537 | ||
|
|
4a8d4346be | ||
|
|
1522baa97d | ||
|
|
a45f068a39 | ||
|
|
ec266d6d9b | ||
|
|
8c64bd64cf | ||
|
|
4dadeaa083 | ||
|
|
ade17c5eaf | ||
|
|
e44686b075 | ||
|
|
55d97404f2 | ||
|
|
e60d5173f4 | ||
|
|
30d4bb923b | ||
|
|
659a2e5a55 | ||
|
|
0b128cc4ab | ||
|
|
a4c1fea08b | ||
|
|
8a50cb9ea3 | ||
|
|
62a6839f84 | ||
|
|
023af864b6 | ||
|
|
d0e167d93c | ||
|
|
33c6e01d34 | ||
|
|
4312791cf6 | ||
|
|
ad1c2b6f0b | ||
|
|
78525a830b | ||
|
|
b02f8e5dc7 | ||
|
|
28998e97ab | ||
|
|
8c3fefd3ff | ||
|
|
819301c7d8 | ||
|
|
76df4d2412 | ||
|
|
6e95a36fb2 | ||
|
|
a6e81e5d09 | ||
|
|
b7e48ac2cc | ||
|
|
35dc1bf028 | ||
|
|
816ff7a8eb | ||
|
|
2b41c97355 | ||
|
|
131def83b9 | ||
|
|
e40211ad49 | ||
|
|
30f8e8b16c | ||
|
|
5a9547e79b | ||
|
|
0d375ecd93 | ||
|
|
970f684345 | ||
|
|
992372913e | ||
|
|
28f328b0bc | ||
|
|
2196551126 | ||
|
|
84653d7aae | ||
|
|
7cb11e5191 | ||
|
|
916b026b12 | ||
|
|
c6406e9f16 | ||
|
|
2b14e25935 | ||
|
|
2f8fd5c359 | ||
|
|
f342fe3351 | ||
|
|
a60f5d1660 | ||
|
|
e6c4f6c703 | ||
|
|
51bf8a5286 | ||
|
|
7df721a863 | ||
|
|
ee7a3e934e | ||
|
|
04254fcb00 | ||
|
|
6911906c4f | ||
|
|
47cf2040f6 | ||
|
|
c47da9acdd | ||
|
|
a11d5a0fa5 | ||
|
|
3c2bb6dfe2 | ||
|
|
d3be1a5bec | ||
|
|
61792e7875 | ||
|
|
c470e0cdcb | ||
|
|
342b84e7ad | ||
|
|
d98a0be80a | ||
|
|
a43907fe14 | ||
|
|
98cc18b12f | ||
|
|
226b0bee68 | ||
|
|
6189e0cb74 | ||
|
|
23470629bd | ||
|
|
d073fb9417 | ||
|
|
d623119eac | ||
|
|
5844de9b3c | ||
|
|
a1a7390d80 | ||
|
|
dd2bc70f47 | ||
|
|
a6acf6527c | ||
|
|
5310a0d6b7 | ||
|
|
a2b931571e | ||
|
|
da9543e426 | ||
|
|
2cfd036466 | ||
|
|
13b058ed1b | ||
|
|
0c8acd32d8 | ||
|
|
fc62a790cd | ||
|
|
eb0724954b | ||
|
|
0168a20e7e | ||
|
|
b394bd3eac | ||
|
|
d0b4fa9328 | ||
|
|
3b50fde76e | ||
|
|
a7ab8deac4 | ||
|
|
a2aa269851 | ||
|
|
d31e1a65a8 | ||
|
|
76cad4ae7e | ||
|
|
cda39c2a05 | ||
|
|
6cc4276288 | ||
|
|
c829915948 | ||
|
|
1dcb56d223 | ||
|
|
90a2e0a9e1 | ||
|
|
65aad0b09a | ||
|
|
b5a3ac6163 | ||
|
|
ed7ed059a2 | ||
|
|
d0a279e04e | ||
|
|
4303e09fe3 | ||
|
|
ac9b04de67 | ||
|
|
e79256e836 | ||
|
|
1dcfcc3531 | ||
|
|
11b1d29df6 | ||
|
|
ad30caa7c8 | ||
|
|
53a03b94d4 | ||
|
|
77da5c973b | ||
|
|
622f64ad29 | ||
|
|
0700e7a736 | ||
|
|
4e1e06c8f7 | ||
|
|
3bb69e356c | ||
|
|
9993366ade | ||
|
|
e5d436c295 | ||
|
|
211157c0d0 | ||
|
|
f83e097769 | ||
|
|
dcacdf65ce | ||
|
|
4b61620538 | ||
|
|
8b0a5e886e | ||
|
|
1045204fb7 | ||
|
|
7e1226bc3d | ||
|
|
b8deb244ca | ||
|
|
6a47d10d7b | ||
|
|
694ac39e89 | ||
|
|
a295f34094 | ||
|
|
6bb66646b8 | ||
|
|
6c0112262e | ||
|
|
4bae4c7491 | ||
|
|
e186356591 | ||
|
|
f3afaf9c51 | ||
|
|
6efd631fd8 | ||
|
|
0aea9ce153 | ||
|
|
4925db1b66 | ||
|
|
72baafe4f2 | ||
|
|
64b98a84f5 | ||
|
|
db3e4d9e83 | ||
|
|
338ba5afdc | ||
|
|
f2ae137947 | ||
|
|
c035636e94 | ||
|
|
f51b80739a | ||
|
|
d1eea7e7ba | ||
|
|
a225cf0f92 | ||
|
|
ae5f85352e | ||
|
|
ffc414062b | ||
|
|
da50071765 | ||
|
|
3b9ea37907 | ||
|
|
1a5710c564 | ||
|
|
9650014506 | ||
|
|
eda7fee2c6 | ||
|
|
79c9c914e2 | ||
|
|
190db26ebd | ||
|
|
857f45f9ec | ||
|
|
5c9a27c930 | ||
|
|
645d70e6ce | ||
|
|
130fc4ee18 | ||
|
|
ea32363144 | ||
|
|
077eb106e3 | ||
|
|
3070dddf96 | ||
|
|
708668df3f | ||
|
|
ea1770b1a7 | ||
|
|
1cfb937490 | ||
|
|
41e1997d73 | ||
|
|
e359042766 | ||
|
|
ae05e58a13 | ||
|
|
ad6f26f6a9 | ||
|
|
2c9b23fc6d | ||
|
|
defaebd064 | ||
|
|
1fb559aedb | ||
|
|
29292956b6 | ||
|
|
ffaf0eb8d5 | ||
|
|
3344453253 | ||
|
|
b60d0e057d | ||
|
|
9d0ffaddc5 | ||
|
|
e1ed1ae69c | ||
|
|
1bb5f10fad | ||
|
|
c0d65afde4 | ||
|
|
e37ac89b88 | ||
|
|
cbd1ecec12 | ||
|
|
536dfb5f2d | ||
|
|
6a288419bc | ||
|
|
721ce4196b | ||
|
|
3a427188dd | ||
|
|
0ca1166f0c | ||
|
|
ec2e04f401 | ||
|
|
d220b49324 | ||
|
|
ec467a81db | ||
|
|
0265bcc038 | ||
|
|
85458cfe5a | ||
|
|
802d084070 | ||
|
|
dfa409da1b | ||
|
|
c05a3c64ee | ||
|
|
17fc231db6 | ||
|
|
9c6eb6e071 | ||
|
|
9034e0787d | ||
|
|
a36b2ed13e | ||
|
|
71647e5bc9 | ||
|
|
7a8c82c226 | ||
|
|
00ef5bc0e6 | ||
|
|
b5686b44c4 | ||
|
|
431f329933 | ||
|
|
0e79da4944 | ||
|
|
915f9f00f5 | ||
|
|
a524ed4439 | ||
|
|
fbfc4c43f4 | ||
|
|
9be7b35bd7 | ||
|
|
b20733d492 | ||
|
|
f69a8af83f | ||
|
|
1145a3a513 | ||
|
|
1c4670e42f | ||
|
|
268ccc2cd5 | ||
|
|
2e342b32f2 | ||
|
|
32737ed9a3 | ||
|
|
a280772be0 | ||
|
|
f75ed58a97 | ||
|
|
f1084158e1 | ||
|
|
aae587c618 | ||
|
|
ac72402af4 | ||
|
|
f8d0281931 | ||
|
|
06e192f595 | ||
|
|
75eb551e22 | ||
|
|
64796da026 | ||
|
|
d00914a11c | ||
|
|
325e404de1 | ||
|
|
e28b683b89 | ||
|
|
82bd0325cf | ||
|
|
a88c8fba76 | ||
|
|
f561f4d67b | ||
|
|
9a02c5394d | ||
|
|
ec09738321 | ||
|
|
2258506383 | ||
|
|
f2af85597e | ||
|
|
e4e08609d9 | ||
|
|
98e16b2e28 | ||
|
|
5af6197c9d | ||
|
|
73c9b94446 | ||
|
|
0ad4af8927 | ||
|
|
ad87aa3eca | ||
|
|
3caf94b919 | ||
|
|
3cc1246bf4 | ||
|
|
d89f732faa | ||
|
|
2f6aa43d40 | ||
|
|
15c9281988 | ||
|
|
2b8f650a6c | ||
|
|
90aa0d8c61 | ||
|
|
ec5e8302d7 | ||
|
|
ce1df9a7ec | ||
|
|
421eb35e2c | ||
|
|
393b7c76bd | ||
|
|
efc4cbc07f | ||
|
|
85f710dafa | ||
|
|
8380c23bca | ||
|
|
7ec77ee40a | ||
|
|
7c0c3ee25b | ||
|
|
b92a6ab04b | ||
|
|
1b19c85611 | ||
|
|
5e1848270f | ||
|
|
e1f262bd26 | ||
|
|
547d36793a | ||
|
|
31e711bd6a | ||
|
|
f2483766cf | ||
|
|
3ea41572bc | ||
|
|
1a29e845b1 | ||
|
|
e7cdc38284 | ||
|
|
69ad66ce49 | ||
|
|
3602256bdf | ||
|
|
99a3c88280 | ||
|
|
fddfa257df | ||
|
|
af7dc5a59a | ||
|
|
31a50bb495 | ||
|
|
f76d2adb7c | ||
|
|
0017763514 | ||
|
|
4cdf98dfaf | ||
|
|
e3c833770a | ||
|
|
1b62113fd3 | ||
|
|
a9010d992e | ||
|
|
2590c7a65f | ||
|
|
ea163384c1 | ||
|
|
93a0503455 | ||
|
|
fb23bd0ac1 | ||
|
|
c32486a213 | ||
|
|
300d7f4662 | ||
|
|
edabd66c98 | ||
|
|
64ef53a3d6 | ||
|
|
cbc006c50a | ||
|
|
fa3ebfc9b5 | ||
|
|
ba637664d5 | ||
|
|
e1ec71b91b | ||
|
|
1498d27a64 | ||
|
|
ddb8921c35 | ||
|
|
5ea35caa91 | ||
|
|
8697472989 | ||
|
|
d40e66f21c | ||
|
|
709cc90bcb | ||
|
|
a9c2a59441 | ||
|
|
220d8e962e | ||
|
|
00df39d2ea | ||
|
|
10904c24a3 | ||
|
|
c06bd35bd5 | ||
|
|
6f4fd0ae2a | ||
|
|
05569ca1ee | ||
|
|
35655673fa | ||
|
|
23c17b1937 | ||
|
|
39369d6eb0 | ||
|
|
5c3a974c90 | ||
|
|
33613369cd | ||
|
|
866cdd3704 | ||
|
|
468b9d655a | ||
|
|
28c78a1339 | ||
|
|
5df48873eb | ||
|
|
b80e5c712a | ||
|
|
b54403163d | ||
|
|
f7bed9089c | ||
|
|
d325ce7a56 | ||
|
|
a3dc449cc9 | ||
|
|
c20093280c | ||
|
|
115720cc53 | ||
|
|
cd1c0cc526 | ||
|
|
8c53689e17 | ||
|
|
d3fe888238 | ||
|
|
333ba13869 | ||
|
|
d77fa6a390 | ||
|
|
19b86606ce | ||
|
|
6ae9f5468a | ||
|
|
df2f48b860 | ||
|
|
daae5ecec6 | ||
|
|
ab0d6ecc1f | ||
|
|
f4ec8352d0 | ||
|
|
fc0777b9ff | ||
|
|
55e492ee5d | ||
|
|
5530adca42 | ||
|
|
7c4e0329bb | ||
|
|
6ff0dd5327 | ||
|
|
9a5b2594f8 | ||
|
|
6f57dab6ba | ||
|
|
5154bbc070 | ||
|
|
c20029003c | ||
|
|
6a9e95b70a | ||
|
|
dfce092dbb | ||
|
|
8b59040a9a | ||
|
|
bb39f66a32 | ||
|
|
019bc27cf2 | ||
|
|
ab6de1752f | ||
|
|
5a9dc2e09a | ||
|
|
de45adf797 | ||
|
|
d308fba3eb | ||
|
|
3c5e4dd944 | ||
|
|
43a7e895a4 | ||
|
|
086ea97365 | ||
|
|
1c9b3e5d22 | ||
|
|
2db32d289f | ||
|
|
4d33823f28 | ||
|
|
73b075d0e7 | ||
|
|
6f93fb29b4 | ||
|
|
0bf80df486 | ||
|
|
b84aecc1c6 | ||
|
|
2ca008861d | ||
|
|
4362291fb7 | ||
|
|
563f818523 | ||
|
|
28b4355d7b | ||
|
|
faca4089b7 | ||
|
|
e7c8fe966f | ||
|
|
9e2447f601 | ||
|
|
3b8059281b | ||
|
|
4103a007db | ||
|
|
e59446c628 | ||
|
|
5e548465dc | ||
|
|
52cbacd306 | ||
|
|
666459c011 | ||
|
|
f6dc955d4b | ||
|
|
a71f4dc0b2 | ||
|
|
939b25efd8 | ||
|
|
729f35b6c1 | ||
|
|
97995f9b8c | ||
|
|
2997deac66 | ||
|
|
a77e12b078 | ||
|
|
f84ec51206 | ||
|
|
421b70b4f6 | ||
|
|
1911d6915c | ||
|
|
b4526df691 | ||
|
|
6e0ba80d12 | ||
|
|
48fdac42f4 | ||
|
|
cb5dca5c69 | ||
|
|
36c699970b | ||
|
|
7ebb70247c | ||
|
|
4a65c70668 | ||
|
|
4f6cddebd3 | ||
|
|
b7a0500f25 | ||
|
|
809b047523 | ||
|
|
ae2c56abce | ||
|
|
68ad165fb1 | ||
|
|
afe040d0b6 | ||
|
|
9a43c1b3ae | ||
|
|
7e58e204ed | ||
|
|
e51d39dcc3 | ||
|
|
93047c5fda | ||
|
|
a5d964a334 | ||
|
|
b5be110f0c | ||
|
|
81ea744e38 | ||
|
|
3e1f5c7632 | ||
|
|
00a2a1ef4d | ||
|
|
ba793ac44e | ||
|
|
49dd3c0659 | ||
|
|
def7ba5faf | ||
|
|
e01eab131f | ||
|
|
0636695052 | ||
|
|
1e893af9df | ||
|
|
0f5c6c0b0c | ||
|
|
f014285143 | ||
|
|
2f194a99d3 | ||
|
|
63094bee7a | ||
|
|
7d2965ee34 | ||
|
|
aabfb0ab1c | ||
|
|
bd321edd20 | ||
|
|
334be5afc7 | ||
|
|
0bc12e744e | ||
|
|
15772e20ac | ||
|
|
75d3972196 | ||
|
|
e89d6f7de3 | ||
|
|
1a5f79732a | ||
|
|
1e2077cf91 | ||
|
|
6dbdfe2fb4 | ||
|
|
e7702f97b0 | ||
|
|
e1c47289c9 | ||
|
|
2efa5f6f14 | ||
|
|
bb1ab54d13 | ||
|
|
f49bd65526 | ||
|
|
37624bec61 | ||
|
|
b4bd18c185 | ||
|
|
8bae5dd93b | ||
|
|
6e98c98c11 | ||
|
|
37e4d49d3d | ||
|
|
48f56beaab | ||
|
|
9cef0d752a | ||
|
|
e0b7061ac3 | ||
|
|
304aa4eeeb | ||
|
|
79b614e5c3 | ||
|
|
af5b20fcd5 | ||
|
|
f30e21ab4b | ||
|
|
6c04084891 | ||
|
|
70adc36089 | ||
|
|
f6b85ce384 | ||
|
|
cce114ce04 | ||
|
|
6ba0577270 | ||
|
|
ac0755c8d3 | ||
|
|
03a17a88a1 | ||
|
|
f593cf4e09 | ||
|
|
80d24dc8a1 | ||
|
|
4b3ce9e701 | ||
|
|
89c6ad5f9d | ||
|
|
99e451005c | ||
|
|
635c898016 | ||
|
|
691c0501ea | ||
|
|
7c8ddb2132 | ||
|
|
22ede8bf66 | ||
|
|
0cebf1552f | ||
|
|
e8fb4f50b3 | ||
|
|
a5ebf637bd | ||
|
|
6e4fac03ec | ||
|
|
58f4c1ee7e | ||
|
|
faec1f969c | ||
|
|
5ab42eaa1d | ||
|
|
ccab7cd9e1 | ||
|
|
24d86684ef | ||
|
|
633a1d644a | ||
|
|
59057e69a3 | ||
|
|
6ce5261dbd | ||
|
|
3b501003ee | ||
|
|
8e87805689 | ||
|
|
33e7caf965 | ||
|
|
5af448c039 | ||
|
|
542b3da250 | ||
|
|
acdb17d28c | ||
|
|
f9e90f0b97 | ||
|
|
4810ba6378 | ||
|
|
e61c8d83f5 | ||
|
|
671340e12f | ||
|
|
3c7ac3156a | ||
|
|
d9f2d17d15 | ||
|
|
7d760fe65b | ||
|
|
53f0cc6f21 | ||
|
|
0a16859c4d | ||
|
|
32795034b5 | ||
|
|
e0b474d2d4 | ||
|
|
e82ee5ac14 | ||
|
|
b4b9a4672d | ||
|
|
c64b70be2e | ||
|
|
cf95c37f39 | ||
|
|
ca855da46e | ||
|
|
92be2a787a | ||
|
|
d166f0afea | ||
|
|
ab1e21079f | ||
|
|
4956dba6ea | ||
|
|
c9701b3631 | ||
|
|
f61e5d8e14 | ||
|
|
773c3fd0d5 | ||
|
|
f0b743a7cf | ||
|
|
5453b8f051 | ||
|
|
dbe440b3ee | ||
|
|
b410285758 | ||
|
|
a591fefcbc | ||
|
|
d4fc353909 | ||
|
|
8c81064453 | ||
|
|
fd553e9940 | ||
|
|
872f411250 | ||
|
|
0cd4993fb0 | ||
|
|
582d4d19e1 | ||
|
|
9a159bd672 | ||
|
|
29a798e2a9 | ||
|
|
ee4e95b7e6 | ||
|
|
11a04ffc15 | ||
|
|
0391bf1cce | ||
|
|
43d53ec2b4 | ||
|
|
b44bf4bb47 | ||
|
|
ecac92d9b3 | ||
|
|
45b6f1a6ef | ||
|
|
d61aeb09c0 | ||
|
|
8fa1843d79 | ||
|
|
3ee7277409 | ||
|
|
1139940c24 | ||
|
|
571e69bb16 | ||
|
|
a5ea35e6b7 | ||
|
|
d4ad66e2a5 | ||
|
|
886c732626 | ||
|
|
c4e0528ce8 | ||
|
|
d4f524769f | ||
|
|
defdeb1f44 | ||
|
|
3b411c657d | ||
|
|
67037c7837 | ||
|
|
dd93588539 | ||
|
|
d7bc7df3d2 | ||
|
|
49d0950a74 | ||
|
|
b3aae7824c | ||
|
|
e0dc5d4ced | ||
|
|
f7099b4555 | ||
|
|
3f8a3f3342 | ||
|
|
2b8afc3972 | ||
|
|
771341f2e9 | ||
|
|
8c928e2b01 | ||
|
|
c7139c1da1 | ||
|
|
0338cc4458 | ||
|
|
8c601f86f6 | ||
|
|
eeae378d69 | ||
|
|
f1fc4013c2 | ||
|
|
d8dafbfac3 | ||
|
|
4537c364ec | ||
|
|
0eb66aebff | ||
|
|
eb4fc9f7ce | ||
|
|
e6ddc61e86 | ||
|
|
c021fd5eb8 | ||
|
|
9b89e79f44 | ||
|
|
79ed54097a | ||
|
|
8647409f9d | ||
|
|
03d9983924 | ||
|
|
dbd65941dc | ||
|
|
b481b629b5 | ||
|
|
0b5a92ebc9 | ||
|
|
942f5dcfbd | ||
|
|
a73667610b | ||
|
|
0e98da8bca | ||
|
|
4dcd80e2ef | ||
|
|
eeec3feb1f | ||
|
|
af70a1f58b | ||
|
|
8ebd6e082e | ||
|
|
ff1e6b835c | ||
|
|
f76992d9de | ||
|
|
d11013c689 | ||
|
|
4b7a020f8e | ||
|
|
a7740757a9 | ||
|
|
e59477801e | ||
|
|
81326c996d | ||
|
|
9fbac2aeaa | ||
|
|
6248cf31c3 | ||
|
|
f3d11dbe5b | ||
|
|
a8c5d7bf2a | ||
|
|
c78d235b07 | ||
|
|
2f5a83e171 | ||
|
|
35d0e73901 | ||
|
|
434573a5f8 | ||
|
|
3810475ee8 | ||
|
|
057f5face3 | ||
|
|
021c133db3 | ||
|
|
9082a89c59 | ||
|
|
70eec78416 | ||
|
|
4e824ae7d2 | ||
|
|
1bac0903a0 | ||
|
|
d0ddadeb13 | ||
|
|
4b77ccb5f6 | ||
|
|
3ea9f7d218 | ||
|
|
84a49752d0 | ||
|
|
736513f8e1 | ||
|
|
e26887036e | ||
|
|
e9773e57b7 | ||
|
|
1d5a3b0184 | ||
|
|
f6d8a85423 | ||
|
|
8e074e2093 | ||
|
|
70bd88e2d9 | ||
|
|
9e053865e1 | ||
|
|
08dc5f5654 | ||
|
|
a1d7f2c774 | ||
|
|
ed564b904f | ||
|
|
b9aef9b317 | ||
|
|
7b9bfaaa18 | ||
|
|
edf6d48e6b | ||
|
|
ba41a27981 | ||
|
|
f72ca1d2ae | ||
|
|
e2ee43fb2d | ||
|
|
6a38654a23 | ||
|
|
3e9e1ee865 | ||
|
|
eb12b7a7ec | ||
|
|
2d23ce0084 | ||
|
|
b94aa70c65 | ||
|
|
0890dcee95 | ||
|
|
6cd44b4564 | ||
|
|
54a56a8cd4 | ||
|
|
80552bc2f1 | ||
|
|
7cc8e606f1 | ||
|
|
eaaf52ae3d | ||
|
|
e8dc787d8c | ||
|
|
c8a213ce86 | ||
|
|
0a1a4a4ad1 | ||
|
|
32644f17fe | ||
|
|
cc3196da95 | ||
|
|
e8e854cfa2 | ||
|
|
1830eb7186 | ||
|
|
d5f93b722c | ||
|
|
7271b6f585 | ||
|
|
5b8342188c | ||
|
|
acae61c26a | ||
|
|
756c272821 | ||
|
|
d71026f7ae | ||
|
|
71edc398d0 | ||
|
|
ab96026176 | ||
|
|
5942df216a | ||
|
|
319b450532 | ||
|
|
364bf3724f | ||
|
|
45cfc56e2d | ||
|
|
b40f460c59 | ||
|
|
c980d1c9d4 | ||
|
|
f961bad4d9 | ||
|
|
a0626b7166 | ||
|
|
81373d2b69 | ||
|
|
18a0ec2f67 | ||
|
|
1009a73e0a | ||
|
|
318377cb7e | ||
|
|
a8ebd94e88 | ||
|
|
3af07078c1 | ||
|
|
b917d242ea | ||
|
|
393eb54794 | ||
|
|
c58d274626 | ||
|
|
c6ce1ef87f | ||
|
|
489b1a0eb4 | ||
|
|
ea3ddee83a | ||
|
|
e80acafc0e | ||
|
|
1619b422b8 | ||
|
|
5b159a0e64 | ||
|
|
b6e80a9e3f | ||
|
|
8e15326b0f | ||
|
|
edca8e15e1 | ||
|
|
3c90fba52e | ||
|
|
dd45035cdc | ||
|
|
7010e9ffd3 | ||
|
|
54af6a5cdb | ||
|
|
8316c9cffa | ||
|
|
6d9772fa9c | ||
|
|
564a799ebf | ||
|
|
c95c915de7 | ||
|
|
7b398459db | ||
|
|
306b4e6a56 | ||
|
|
d425e7678a | ||
|
|
1bd18eece0 | ||
|
|
674c84f90a | ||
|
|
d4584c47c2 | ||
|
|
cffe0d3e80 | ||
|
|
be4452ec59 | ||
|
|
8a4b27d8ae | ||
|
|
8a34c8330f | ||
|
|
7bd0161243 | ||
|
|
5bf30ba4d2 | ||
|
|
9be00788fb | ||
|
|
253082f396 | ||
|
|
7c4d4519a6 | ||
|
|
07b0169f8e | ||
|
|
14f3d77e7e | ||
|
|
f5668478d5 | ||
|
|
17896501cf | ||
|
|
21835f574d | ||
|
|
9b029280fd | ||
|
|
4e1ff6af7d | ||
|
|
e339c9b0a9 | ||
|
|
38dc30f9b9 | ||
|
|
d291965a58 | ||
|
|
edb1d697fe | ||
|
|
8b2fbc409c | ||
|
|
4025c31570 | ||
|
|
7518fa2bf3 | ||
|
|
aa830919db | ||
|
|
2a91d26728 | ||
|
|
b4de00da0b | ||
|
|
6c8a00df5d | ||
|
|
f109575ed1 | ||
|
|
0bd06961d9 | ||
|
|
bc40d17e5a | ||
|
|
becdb36e84 | ||
|
|
1e7cd00e2d | ||
|
|
7e44663112 | ||
|
|
3053186479 | ||
|
|
b0c0cecb05 | ||
|
|
58a897dd6d | ||
|
|
28023c7530 | ||
|
|
f978d94c4b | ||
|
|
66d0ef00b3 | ||
|
|
eea735f4b2 | ||
|
|
55287f9385 | ||
|
|
851c86d032 | ||
|
|
10ff95fa6b | ||
|
|
f1b2c78695 | ||
|
|
303e7c74c3 | ||
|
|
27a6313ce1 | ||
|
|
7d39889cef | ||
|
|
afd1c6513f | ||
|
|
ec86ca0fdd | ||
|
|
4e0d799d98 | ||
|
|
fed16e5be9 | ||
|
|
5a50cd3c16 | ||
|
|
f436fa895b | ||
|
|
fa66720fc7 | ||
|
|
a7bb112c70 | ||
|
|
d117e34a79 | ||
|
|
52e103d9b3 | ||
|
|
0bedf7a901 | ||
|
|
04ec7965c1 | ||
|
|
789ed72689 | ||
|
|
eebb1adbd8 | ||
|
|
d4d75684b4 | ||
|
|
951a5dc978 | ||
|
|
7fab48ba9b | ||
|
|
b2088e7cd2 | ||
|
|
04a39c34c6 | ||
|
|
9e786bbdff | ||
|
|
6e6af1c6c4 | ||
|
|
394c048b21 | ||
|
|
94e255b198 | ||
|
|
8a41f8f476 | ||
|
|
c680df8738 | ||
|
|
49f050655c | ||
|
|
f270213bb7 | ||
|
|
6ed064eca6 | ||
|
|
d37f9a6ea1 | ||
|
|
ea7121f9d9 | ||
|
|
9af8fddaf3 | ||
|
|
b81fba82a5 | ||
|
|
fc220206d7 | ||
|
|
2686318c11 | ||
|
|
869d32785d | ||
|
|
1e14373438 | ||
|
|
e637874b85 | ||
|
|
0a1efba7d9 | ||
|
|
e7dca7ff6f | ||
|
|
93b552b33c | ||
|
|
59a36015b6 | ||
|
|
1ccf93b64b | ||
|
|
b702effb60 | ||
|
|
882218b493 | ||
|
|
2ccfadc075 | ||
|
|
9bf4bacaee | ||
|
|
e23f807e63 | ||
|
|
b959f61049 | ||
|
|
35245d5268 | ||
|
|
0781113fd9 | ||
|
|
17e6c65652 | ||
|
|
2c15be8351 | ||
|
|
fb3e5a68aa | ||
|
|
ae17432ee3 | ||
|
|
a564f49381 | ||
|
|
e300bba4de | ||
|
|
23a5377e25 | ||
|
|
ceaf19b9a1 | ||
|
|
a144096ed3 | ||
|
|
876083b085 | ||
|
|
7a4aa94b8b | ||
|
|
f7e3497bd0 | ||
|
|
df61e5d9aa | ||
|
|
d5872df7ed | ||
|
|
dcecbd4e37 | ||
|
|
9f7e02933f | ||
|
|
0b64ffb4c7 | ||
|
|
51655cffe6 | ||
|
|
fffa4cf157 | ||
|
|
c74b0a017b | ||
|
|
70301e8e91 | ||
|
|
b88b0ec271 | ||
|
|
8ac13c0d6d | ||
|
|
79c43d7c9f | ||
|
|
e9c02edfae | ||
|
|
47ea767348 | ||
|
|
718762f0e0 | ||
|
|
025da50f14 | ||
|
|
d630e34249 | ||
|
|
320b3cc885 | ||
|
|
6d5d8efe1f | ||
|
|
e75bef451b | ||
|
|
12c84196f3 | ||
|
|
749b2ba7cb | ||
|
|
828e96fa90 | ||
|
|
c3643bb1e2 | ||
|
|
59bb6d67a0 | ||
|
|
e11da6d01e | ||
|
|
ea2764c5ba | ||
|
|
120b3533bc | ||
|
|
5e00a60eaa | ||
|
|
9d20ab4dad | ||
|
|
b06b9ea379 | ||
|
|
5bc415513b | ||
|
|
9948fd4bf4 | ||
|
|
ff66f3c8dd | ||
|
|
f75f88435c | ||
|
|
4efd0c96d1 | ||
|
|
ca63bbc47c | ||
|
|
f20ed3468a | ||
|
|
9de8501277 | ||
|
|
1c68daeb39 | ||
|
|
d0968696a6 | ||
|
|
71278fe390 | ||
|
|
71064e0f92 | ||
|
|
3a93d723ea | ||
|
|
88c12ba712 | ||
|
|
cfb7fc237d | ||
|
|
84a521869b | ||
|
|
86adb641be | ||
|
|
3d340e7b9f | ||
|
|
e2d7024974 | ||
|
|
635ce0fbda | ||
|
|
a438cc75b5 | ||
|
|
bf728971c0 | ||
|
|
2e45c1ace2 | ||
|
|
ebf9d99127 | ||
|
|
773c95af58 | ||
|
|
1990a54d5d | ||
|
|
b01f950a28 | ||
|
|
d66e041db3 | ||
|
|
6b9cd02de8 | ||
|
|
572ecef291 | ||
|
|
4968d3efa6 | ||
|
|
7d4f334ec7 | ||
|
|
4a11faf7d1 | ||
|
|
88f8840ef5 | ||
|
|
ce99b1093a | ||
|
|
ab39d632fd | ||
|
|
7be473f99d | ||
|
|
2c5a845d1a | ||
|
|
86e81d3584 | ||
|
|
2120d4493e | ||
|
|
57c9c2fe90 | ||
|
|
0251d5776e | ||
|
|
b0d5834396 | ||
|
|
7b837b4468 | ||
|
|
67064b3275 | ||
|
|
6f60bb264a | ||
|
|
b560df49a3 | ||
|
|
d8b31ec4bc | ||
|
|
90fc791aac | ||
|
|
64928efb54 | ||
|
|
9ca0d7244e | ||
|
|
acb412b8f1 | ||
|
|
4dd52e6f82 | ||
|
|
23fcaaf5d0 | ||
|
|
cd9702fd03 | ||
|
|
082cfc164d | ||
|
|
b94181c796 | ||
|
|
13ad244be1 | ||
|
|
ec1ad333cd | ||
|
|
ac75e554a9 | ||
|
|
c249ed5f9f | ||
|
|
795c071e64 | ||
|
|
fc130ebfb5 | ||
|
|
a0bf4f2da0 | ||
|
|
2f57a39fcc | ||
|
|
211d6c793a | ||
|
|
79344ddad2 | ||
|
|
50a551f875 | ||
|
|
884715054b | ||
|
|
9a3bc52ffb | ||
|
|
7615f333e1 | ||
|
|
d9913ffb1e | ||
|
|
534a161dc0 | ||
|
|
83b1965733 | ||
|
|
d9df1243b6 | ||
|
|
b88365e475 | ||
|
|
090d4dc2c5 | ||
|
|
97e17a7754 | ||
|
|
a5db48c719 | ||
|
|
65d65c9e5f | ||
|
|
c5c3591471 | ||
|
|
a47c3cf004 | ||
|
|
f76c60e867 | ||
|
|
58bba637ca | ||
|
|
59063bc194 | ||
|
|
2e33b3162c | ||
|
|
1e7755b877 | ||
|
|
7b8e92ace2 | ||
|
|
e25247d0e5 | ||
|
|
93dc3338c1 | ||
|
|
fcd392eec1 | ||
|
|
81b002ad63 | ||
|
|
70feec2f5a | ||
|
|
4ddb73e8d2 | ||
|
|
ca099e077b | ||
|
|
30048e7a82 | ||
|
|
2ee05f46e7 | ||
|
|
3ea30a4461 | ||
|
|
83bae5ad74 | ||
|
|
4027e14266 | ||
|
|
a84da053a6 | ||
|
|
1b0955b959 | ||
|
|
bbcea47bfd | ||
|
|
88f656f937 | ||
|
|
148e569182 | ||
|
|
d8405e2fc8 | ||
|
|
594ce263b6 | ||
|
|
73a623cc1b | ||
|
|
e0882ab6b5 | ||
|
|
c93d9d909d | ||
|
|
8e5775dd2c | ||
|
|
dd38c0d581 | ||
|
|
f01df7bdd2 | ||
|
|
ed16e6f4e9 | ||
|
|
e9217f3261 | ||
|
|
d2e4c53c20 | ||
|
|
c402ae645e | ||
|
|
9c69a33734 | ||
|
|
47eb18142d | ||
|
|
0ca97727ad | ||
|
|
cd0cf2106a | ||
|
|
edc921bf98 | ||
|
|
b67c060211 | ||
|
|
cb5791276a | ||
|
|
d20317dd62 | ||
|
|
9cfc42156d | ||
|
|
52d8e3d54f | ||
|
|
186b56c335 | ||
|
|
006bbf8fa0 | ||
|
|
ec6ccb69dd | ||
|
|
81af09d86f | ||
|
|
4299491104 | ||
|
|
ad693eb71f | ||
|
|
02eb8f632f | ||
|
|
352236c390 | ||
|
|
cb5f5938f7 | ||
|
|
ed4583ee7a | ||
|
|
d4b09f39ad | ||
|
|
25ab2ca2dc | ||
|
|
c6714fcc4e | ||
|
|
0f558f0479 | ||
|
|
4ca87ab84e | ||
|
|
90d9c45134 | ||
|
|
193f1f8b23 | ||
|
|
3415d76b40 | ||
|
|
a5b9512fdb | ||
|
|
8db64ffd0a | ||
|
|
9a6f407a6d | ||
|
|
26c0df6e05 | ||
|
|
420d31f825 | ||
|
|
72ca147b73 | ||
|
|
f1a001a834 | ||
|
|
2b704e16c7 | ||
|
|
2c55a9d86c | ||
|
|
e4d45a1369 | ||
|
|
eebeb4a213 | ||
|
|
c70791b25c | ||
|
|
9aa6990823 | ||
|
|
67b04aa48b | ||
|
|
0dc62d67ef | ||
|
|
f3e91e22d9 | ||
|
|
f8f3f640e3 | ||
|
|
63a1fe0b9e | ||
|
|
d9dffe0151 | ||
|
|
6f28d0721e | ||
|
|
61eef2b86f | ||
|
|
74d0e4c4dc | ||
|
|
2b40b6e560 | ||
|
|
86878541f3 | ||
|
|
b0a786970f | ||
|
|
92e548462d | ||
|
|
d05e1e59dc | ||
|
|
9638153581 | ||
|
|
0e06251424 | ||
|
|
c5c04e762f | ||
|
|
e361f09691 | ||
|
|
1b1eb6636d | ||
|
|
80a2d902c0 | ||
|
|
6940eec453 | ||
|
|
aaf27f2d60 | ||
|
|
9cb1f2787c | ||
|
|
c047ec8588 | ||
|
|
2b54468923 | ||
|
|
e8f257d2cf | ||
|
|
66763e537d | ||
|
|
0eee296664 | ||
|
|
2891133c5c | ||
|
|
6231775997 | ||
|
|
61ff4d50fd | ||
|
|
13560e3b53 | ||
|
|
52ae852953 | ||
|
|
7761706fb5 | ||
|
|
d25437c8e0 | ||
|
|
6883ef1e9f | ||
|
|
6bb9dd1028 | ||
|
|
d6278541dd | ||
|
|
5977598c71 | ||
|
|
35706d71f7 | ||
|
|
c38f0900d1 | ||
|
|
23ed82c0ba | ||
|
|
ea4b82f11e | ||
|
|
8cfff4299a | ||
|
|
56250bea63 | ||
|
|
d4d376c8c6 | ||
|
|
0146357eed | ||
|
|
9759264aec | ||
|
|
c17465a2a7 | ||
|
|
dc3795d1cd | ||
|
|
fdf2b26fdf | ||
|
|
eff0012ea1 | ||
|
|
d74186e93c | ||
|
|
fe108bb354 | ||
|
|
9fb556f180 | ||
|
|
f1db204e0d | ||
|
|
fd35c8d862 | ||
|
|
4d134e5087 | ||
|
|
1e3b4e34e5 | ||
|
|
95a02bece7 | ||
|
|
62ffb786b2 | ||
|
|
4d41ed60c9 | ||
|
|
fc8624c93b | ||
|
|
800a77b2e0 | ||
|
|
361443d845 | ||
|
|
7db9a6730a | ||
|
|
5a784e6c9b | ||
|
|
91c16a319c | ||
|
|
47ebfec59e | ||
|
|
f582469ba2 | ||
|
|
06bc5e074a | ||
|
|
d4c3dc4eab | ||
|
|
cfeb07c65f | ||
|
|
ced56ec4cd | ||
|
|
3cf215dbd0 | ||
|
|
f237a8d29a | ||
|
|
4d5a1a152f | ||
|
|
6735bf3e76 | ||
|
|
85adc466b3 | ||
|
|
afc02a856d | ||
|
|
21d6b02410 | ||
|
|
5b0b719ce9 | ||
|
|
9ce0315c3e | ||
|
|
6fe34ec618 | ||
|
|
05a30d0129 | ||
|
|
6cf2c3459c | ||
|
|
e87513bb45 | ||
|
|
0f5c1b8e8e | ||
|
|
c109ac76a7 | ||
|
|
d7bca1c232 | ||
|
|
0a034f9acb | ||
|
|
98774c408c | ||
|
|
802036f693 | ||
|
|
5bb9ac6fcd | ||
|
|
fcc02ed600 | ||
|
|
1e45d14ec3 | ||
|
|
97027e657e | ||
|
|
487fec0258 | ||
|
|
b25bc72f3e | ||
|
|
d086e4d692 | ||
|
|
8d2d66abb9 | ||
|
|
6ca1269261 | ||
|
|
d374da0481 | ||
|
|
83242e1fa9 | ||
|
|
35fa4e7d8f | ||
|
|
5022e96a48 | ||
|
|
1779c8f84e | ||
|
|
74d9791f03 | ||
|
|
a6cb96c758 | ||
|
|
b05463ad6c | ||
|
|
f229fbc4b1 | ||
|
|
8d4f72c691 | ||
|
|
1d470f87d4 | ||
|
|
04260a7f45 | ||
|
|
3c03199ea3 | ||
|
|
acd68f86d1 | ||
|
|
23e312f2ba | ||
|
|
3931903018 | ||
|
|
8c680da98c | ||
|
|
21e682f54d | ||
|
|
70593c2b6a | ||
|
|
6b64667767 | ||
|
|
96d1777132 | ||
|
|
f6bd725633 | ||
|
|
dd7e2d933f | ||
|
|
97177e379e | ||
|
|
8df00a18e8 | ||
|
|
6668fa5505 | ||
|
|
c348ffa34e | ||
|
|
275e1be6be | ||
|
|
c18924a350 | ||
|
|
8325893369 | ||
|
|
66fd90332d | ||
|
|
4dad9e3478 | ||
|
|
0c005bb3ff | ||
|
|
57b9a1b4c7 | ||
|
|
b29b1b3630 | ||
|
|
3a071ea071 | ||
|
|
25efe15329 | ||
|
|
d544d69524 | ||
|
|
bcc0fc9e00 | ||
|
|
7f2d2d6588 | ||
|
|
2e70eb6718 | ||
|
|
b7601fded3 | ||
|
|
13ace49230 | ||
|
|
229ecc9ba7 | ||
|
|
602f761552 | ||
|
|
f271c7a74e | ||
|
|
c47242e40e | ||
|
|
887f2f9a52 | ||
|
|
6b528a6d99 | ||
|
|
7ff8564077 | ||
|
|
77e79653ba | ||
|
|
4665503caa | ||
|
|
1f922362a1 | ||
|
|
f5e095f345 | ||
|
|
a392fc43f9 | ||
|
|
d444e8896c | ||
|
|
935d42af74 | ||
|
|
a9e15c2d33 | ||
|
|
b5ae52a3b6 | ||
|
|
abf405fd20 | ||
|
|
3215eb9a8a | ||
|
|
f2ddd697e9 | ||
|
|
9e6fc59505 | ||
|
|
2dc0e10c34 | ||
|
|
2a086c8b64 | ||
|
|
cf9e3a5f1d | ||
|
|
91616396b7 | ||
|
|
300e3fd4d3 | ||
|
|
373e11bc41 | ||
|
|
4cb140512a | ||
|
|
bb27eab819 | ||
|
|
d99dd5f19f | ||
|
|
d60d350fcb | ||
|
|
b1e7e407a7 | ||
|
|
4e7d338144 | ||
|
|
4528541b3f | ||
|
|
a829dc72bc | ||
|
|
9e58ec0f80 | ||
|
|
606f52e493 | ||
|
|
769f6ea976 | ||
|
|
b6395aa463 | ||
|
|
3f1d6238ff | ||
|
|
89eed17839 | ||
|
|
e990c3648e | ||
|
|
67f9be5d4d | ||
|
|
8650a147aa | ||
|
|
8490f8d90b | ||
|
|
8f1c7bf40a | ||
|
|
ec87b64d0c | ||
|
|
27eee4c8fa | ||
|
|
af32152bcc | ||
|
|
e72edb95b5 | ||
|
|
efed143a11 | ||
|
|
99f64d7565 | ||
|
|
32ef472b0c | ||
|
|
7ade1b9a52 | ||
|
|
cc664d1280 | ||
|
|
84874601a9 | ||
|
|
b47d528bf0 | ||
|
|
59612c45ea | ||
|
|
e8be6f674e | ||
|
|
33554dd9cc | ||
|
|
4dd73930e3 | ||
|
|
af14c33e39 | ||
|
|
7e9312a93c | ||
|
|
4048ba18f3 | ||
|
|
3ed563d3f1 | ||
|
|
11aef9f054 | ||
|
|
556a8e7176 | ||
|
|
465066c04c | ||
|
|
668eaa1a7c | ||
|
|
0bed673340 | ||
|
|
13f606999c | ||
|
|
b0677ca006 | ||
|
|
22846db1a3 | ||
|
|
c9b634a274 | ||
|
|
c37a492cb7 | ||
|
|
92f663c753 | ||
|
|
35a2f9bf2b | ||
|
|
2dcf4b23b7 | ||
|
|
22b2f30ffb | ||
|
|
9a7215a2a2 | ||
|
|
6d4cc6efcb | ||
|
|
45a0c54fb8 | ||
|
|
e8c9c8b90c | ||
|
|
367fcad53c | ||
|
|
3a6c5b0cd5 | ||
|
|
375adab087 | ||
|
|
fbf0f98c3a | ||
|
|
711342829b | ||
|
|
daf3be6346 | ||
|
|
06edf9351a | ||
|
|
ead64864f4 | ||
|
|
8521387312 | ||
|
|
3e5b47185e | ||
|
|
b5dbb7da90 | ||
|
|
03886905bd | ||
|
|
2b688a99ff | ||
|
|
f01282f12f | ||
|
|
5e5fca9722 | ||
|
|
2ff32d3b90 | ||
|
|
3b72bd18f0 | ||
|
|
fcb7f89001 | ||
|
|
38cebf52c3 | ||
|
|
56983b2553 | ||
|
|
04b347b968 | ||
|
|
08a87ea112 | ||
|
|
67e8046af6 | ||
|
|
dc11fad342 | ||
|
|
8f9857e72d | ||
|
|
76bcbc632e | ||
|
|
e04d88977e | ||
|
|
3e613a8b80 | ||
|
|
1f364168e8 | ||
|
|
07439cfeeb | ||
|
|
94eda92d21 | ||
|
|
15715cec10 | ||
|
|
3662bb8e84 | ||
|
|
14fc75be44 | ||
|
|
a489e0845b | ||
|
|
acf7a23965 | ||
|
|
a96a2fd825 | ||
|
|
d455653f11 | ||
|
|
8579ae99a0 | ||
|
|
da5cb3d905 | ||
|
|
81315e3db4 | ||
|
|
8a880a3f89 | ||
|
|
d5da9a80f6 | ||
|
|
670966be57 | ||
|
|
5879690fc0 | ||
|
|
ff054d44b7 | ||
|
|
e909cfc744 | ||
|
|
b51ca84a8f | ||
|
|
3a9af0480d | ||
|
|
ebd0a8096c | ||
|
|
a1f2f8d944 | ||
|
|
0a4dc6961d | ||
|
|
2e196fc41b | ||
|
|
844a7b5d76 | ||
|
|
ce343ffcdb | ||
|
|
25972aaa29 | ||
|
|
0fe8c1f121 | ||
|
|
cb37cf0889 | ||
|
|
b5721a7e36 | ||
|
|
93b40ad867 | ||
|
|
a3e5e40385 | ||
|
|
e13ed6f46e | ||
|
|
65dffbbe3a | ||
|
|
16dafc9c57 | ||
|
|
bcd2f95e4d | ||
|
|
51ae7fee7e | ||
|
|
2eb276f995 | ||
|
|
6a98f9947a | ||
|
|
66cbd1a30e | ||
|
|
c8d1b56d9e | ||
|
|
457ce2958e | ||
|
|
b7b6d5672e | ||
|
|
7dac6197e3 | ||
|
|
ea40b439ee | ||
|
|
7061f7f26a | ||
|
|
de1342a5d7 | ||
|
|
2f976dbeb3 | ||
|
|
e37e242bae | ||
|
|
78c676e858 | ||
|
|
9e74ef9e37 | ||
|
|
1740be22ae | ||
|
|
35c499a074 | ||
|
|
048093522a | ||
|
|
66c696bfec | ||
|
|
5dc5082c4e | ||
|
|
42f79630e6 | ||
|
|
f8ccf0a91e | ||
|
|
b924c1a80c | ||
|
|
42a6340a95 | ||
|
|
8b2b1d453d | ||
|
|
e70207fbac | ||
|
|
846db416ec | ||
|
|
84288aafe9 | ||
|
|
d2d4318659 | ||
|
|
324c3000a0 | ||
|
|
fc6d1d2ab8 | ||
|
|
7fb12fa5b3 | ||
|
|
791e53a935 | ||
|
|
3a705c3e5f | ||
|
|
ca61291995 | ||
|
|
a875557000 | ||
|
|
517590883c | ||
|
|
ffa58f71f0 | ||
|
|
9017f1488f | ||
|
|
c6bac3a247 | ||
|
|
a7e48e8af0 | ||
|
|
41b73da329 | ||
|
|
48d4016f8d | ||
|
|
ed3624ce7b | ||
|
|
58eebd8bfd | ||
|
|
cc95200289 | ||
|
|
7035993a3f | ||
|
|
663b10f767 | ||
|
|
922e45a80d | ||
|
|
3dca3752bc | ||
|
|
336ca104e9 | ||
|
|
7492e7d450 | ||
|
|
7440702fea | ||
|
|
e568f84dcb | ||
|
|
d043161bdd | ||
|
|
9768b80736 | ||
|
|
93185e7e84 | ||
|
|
18a717e90a | ||
|
|
99ce255da8 | ||
|
|
770e328865 | ||
|
|
2776eaf640 | ||
|
|
2d825e4095 | ||
|
|
64cac6cd5a | ||
|
|
8d7eccf92e | ||
|
|
01cd80cc2f | ||
|
|
801e218912 | ||
|
|
c1cb19a139 | ||
|
|
419f4a354f | ||
|
|
569e884a2c | ||
|
|
647e9f6a14 | ||
|
|
a24cffa9df | ||
|
|
58d0921696 | ||
|
|
2b3e39cbb8 | ||
|
|
689e5eecc4 | ||
|
|
36917c0d18 | ||
|
|
4421766850 | ||
|
|
2d2af55849 | ||
|
|
906fef89f9 | ||
|
|
479a468187 | ||
|
|
813e8deb8f | ||
|
|
4c4259ed90 | ||
|
|
b186958422 | ||
|
|
83b41e1479 | ||
|
|
97477644fd | ||
|
|
f3e4f0d1fb | ||
|
|
4175369f96 | ||
|
|
f2fcfe582f | ||
|
|
b2d546be2f | ||
|
|
a7db0dd75e | ||
|
|
94127c1b76 | ||
|
|
220690d4d0 | ||
|
|
fb3ffe679b | ||
|
|
b38f7fe4f5 | ||
|
|
110eb2b072 | ||
|
|
d474e6b8bb | ||
|
|
5b074f4c1c | ||
|
|
8e4ab4d398 | ||
|
|
7eb934d5d3 | ||
|
|
c3b655f675 | ||
|
|
068efaeba7 | ||
|
|
ff7a0f1ae6 | ||
|
|
8b26deb477 | ||
|
|
21fedd0d50 | ||
|
|
ecd4a5dedd | ||
|
|
047ee71259 | ||
|
|
1b97eb777c | ||
|
|
cd31c4e513 | ||
|
|
1582f5a7bd | ||
|
|
32facbbc74 | ||
|
|
7696b9d69c | ||
|
|
d2e1d5aacb | ||
|
|
e70affe6c3 | ||
|
|
d38f9c39e4 | ||
|
|
f7209ef32c | ||
|
|
1f10ba936a | ||
|
|
0332975a9a | ||
|
|
369c4f77a0 | ||
|
|
d8a284ebb9 | ||
|
|
dca62ce725 | ||
|
|
1c64ebd05b | ||
|
|
0727c83a43 | ||
|
|
eb4a8caefc | ||
|
|
253efc6c6d | ||
|
|
d8be06c165 | ||
|
|
e02baac897 | ||
|
|
46f19b6079 | ||
|
|
2f3ba53a89 | ||
|
|
b763e06560 | ||
|
|
2447ea4fb0 | ||
|
|
75f86352be | ||
|
|
19dd58d1d3 | ||
|
|
68843db5b8 | ||
|
|
586c2eb9b0 | ||
|
|
156cdf812d | ||
|
|
5e6d9749fb | ||
|
|
2392aeeb0c | ||
|
|
6d89addb67 | ||
|
|
5a29b05e88 | ||
|
|
90b46f4c0f | ||
|
|
1281af0994 | ||
|
|
b31069866a | ||
|
|
00f896bbb4 | ||
|
|
cd5c03926f | ||
|
|
11218913a4 | ||
|
|
0a039e4f53 | ||
|
|
7e37b3d760 | ||
|
|
d577c229e3 | ||
|
|
6cd74946e4 | ||
|
|
d34b6428ae | ||
|
|
e242e3f371 | ||
|
|
4165004491 | ||
|
|
d290d5fbf4 | ||
|
|
81c99209ce | ||
|
|
1962aac95b | ||
|
|
60a66585f3 | ||
|
|
d7f3842a30 | ||
|
|
81be883bee | ||
|
|
0dfcb225e0 | ||
|
|
06f95efd52 | ||
|
|
0dae411333 | ||
|
|
c1e1b6f29a | ||
|
|
a511bdbcd8 | ||
|
|
b0037faa11 | ||
|
|
6dc6a7a29c | ||
|
|
fd6192a88e | ||
|
|
fcb8e1f8c9 | ||
|
|
2da04fed3d | ||
|
|
00abdccc13 | ||
|
|
9f25644645 | ||
|
|
83b84dfff7 | ||
|
|
c0f5f0567b | ||
|
|
f53a54a126 | ||
|
|
e3ac4433e1 | ||
|
|
1e831513a0 | ||
|
|
a40a6e8023 | ||
|
|
22982178e2 | ||
|
|
63e2854d7a | ||
|
|
7117580446 | ||
|
|
691562324b | ||
|
|
f0e6e20732 | ||
|
|
67ab3e1e01 | ||
|
|
a048ab3327 | ||
|
|
af1be79f78 | ||
|
|
81366a1e14 | ||
|
|
21c4e2eeda | ||
|
|
b866338197 | ||
|
|
98e19db3fc | ||
|
|
5221d871a0 | ||
|
|
d48c93938f | ||
|
|
0aa3583412 | ||
|
|
38adbb7386 | ||
|
|
0ecdb07a0a | ||
|
|
5191777fca | ||
|
|
e1feff66b4 | ||
|
|
e96f0e24b3 | ||
|
|
891b2580c0 | ||
|
|
8fa477a3f2 | ||
|
|
48079df235 | ||
|
|
4bd796c84b | ||
|
|
efa38ea80a | ||
|
|
b0be025f93 | ||
|
|
08bc3170ad | ||
|
|
b126a97e23 | ||
|
|
505940d9b4 | ||
|
|
41fbedbdb9 | ||
|
|
00379f8cd0 | ||
|
|
52e9772ef9 | ||
|
|
634a9e7a6c | ||
|
|
8383aeeddd | ||
|
|
308c07c2b3 | ||
|
|
ad352beb24 | ||
|
|
1e59b75d08 | ||
|
|
bfef8b125c | ||
|
|
b3f1bad6b7 | ||
|
|
8e3e38b72f | ||
|
|
714f21913b | ||
|
|
6e1733821c | ||
|
|
d5c285bb63 | ||
|
|
2be734f137 | ||
|
|
67ab92359f | ||
|
|
89184b97dd | ||
|
|
466eac71a1 | ||
|
|
ce35c6232e | ||
|
|
2e51375c0f | ||
|
|
e38b34e391 | ||
|
|
2e0af3c3fe | ||
|
|
ab1550e24b | ||
|
|
7d57af98fd | ||
|
|
f1cb6b7b8e | ||
|
|
2cb03f1671 | ||
|
|
da68b22601 | ||
|
|
c68f1b8a03 | ||
|
|
02ae5e301d | ||
|
|
0dc7a4af2d | ||
|
|
4d8bb5a844 | ||
|
|
a8ffdecef0 | ||
|
|
adf62cc5f5 | ||
|
|
4aaafdf106 | ||
|
|
95e5f06554 | ||
|
|
361604d7f6 | ||
|
|
97292d5a7f | ||
|
|
3a552823c8 | ||
|
|
a36c06ff2f | ||
|
|
fe7cce1726 | ||
|
|
b4e5313160 | ||
|
|
151fb40e23 | ||
|
|
a111341293 | ||
|
|
4df7554d81 | ||
|
|
e58b0c219f | ||
|
|
e551c98ca4 | ||
|
|
0bd954e0c1 | ||
|
|
23f13ca87e | ||
|
|
2f66df36a7 | ||
|
|
03cc62f4df | ||
|
|
31fb23b83b | ||
|
|
445c87812d | ||
|
|
25b3ffbb6e | ||
|
|
358f5c6046 | ||
|
|
4fdc38b53f | ||
|
|
9fc669d897 | ||
|
|
f40f868429 | ||
|
|
43bdd23b3c | ||
|
|
0bf12f0965 | ||
|
|
5434605984 | ||
|
|
19a1bf0c0e | ||
|
|
16adb81cd2 | ||
|
|
115b77682b | ||
|
|
82839cc500 | ||
|
|
5dca460399 | ||
|
|
0eeb68737c | ||
|
|
389e21aca7 | ||
|
|
1eec2905b1 | ||
|
|
7823368ebc | ||
|
|
8b0f5ff91e | ||
|
|
f45a4d06a3 | ||
|
|
d16a197651 | ||
|
|
95263720f3 | ||
|
|
56f05c54f9 | ||
|
|
b20c5acf75 | ||
|
|
33f296da7c | ||
|
|
429ac3c072 | ||
|
|
4b5a555c1e | ||
|
|
278d64d800 | ||
|
|
6ceb08d41d | ||
|
|
0e513614c3 | ||
|
|
b8ac0819aa | ||
|
|
8380da7a10 | ||
|
|
cb7922fe15 | ||
|
|
3b6cfef67d | ||
|
|
b20bb2d023 | ||
|
|
f8decaac41 | ||
|
|
d7674c15b5 | ||
|
|
9d1c6eaa7f | ||
|
|
1927a94879 | ||
|
|
590b877f76 | ||
|
|
6c93862891 | ||
|
|
cd9289bcdd | ||
|
|
c294e1f6df | ||
|
|
9090cc70bf | ||
|
|
244e2a9e8d | ||
|
|
cc2b2921d9 | ||
|
|
88339864f7 | ||
|
|
f0ad9083a5 | ||
|
|
e66194fb9d | ||
|
|
042693bf53 | ||
|
|
4a8cecd225 | ||
|
|
1a9003c999 | ||
|
|
e35f09ea2f | ||
|
|
056d66b865 | ||
|
|
ceafaf5e2e | ||
|
|
5207e697dd | ||
|
|
39a5da9a1b | ||
|
|
4ed99e026f | ||
|
|
cdff15dade | ||
|
|
16f2b16d22 | ||
|
|
3efde7a2e3 | ||
|
|
032bcec313 | ||
|
|
a5d5ed8524 | ||
|
|
c1e5d346c8 | ||
|
|
0a004b8f7e | ||
|
|
ffcd5b2f2c | ||
|
|
11ed680b5a | ||
|
|
3abc22a855 | ||
|
|
ef9d6203c0 | ||
|
|
016df27726 | ||
|
|
246ce114e2 | ||
|
|
f3767417b0 | ||
|
|
67fb62ec52 | ||
|
|
27f2dcc8d5 | ||
|
|
9f37393a69 | ||
|
|
a7af6ecfc0 | ||
|
|
f772ab5fcc | ||
|
|
6076954881 | ||
|
|
581bb74b9c | ||
|
|
81a8c93db4 | ||
|
|
6d8e162df3 | ||
|
|
9f44b46186 | ||
|
|
e15f826190 | ||
|
|
f5ea2c0345 | ||
|
|
fff0051540 | ||
|
|
0fd3df021a | ||
|
|
44bbcfeebb | ||
|
|
5ae8e4c391 | ||
|
|
1e039cefcb | ||
|
|
eeafd84625 | ||
|
|
6955ad861c | ||
|
|
9fff6d70fe | ||
|
|
742e718c43 | ||
|
|
d3020d591c | ||
|
|
ab44402886 | ||
|
|
13cf87fa7a | ||
|
|
c9a2ccb9f9 | ||
|
|
3f78a5d595 | ||
|
|
cdfdcba477 | ||
|
|
1dbbfb5112 | ||
|
|
d528d3854b | ||
|
|
f6b5467663 | ||
|
|
77e22b355b | ||
|
|
74b2b1d4cf | ||
|
|
9736d6c3e4 | ||
|
|
4c8f1be770 | ||
|
|
3f88293d8e | ||
|
|
40828d8f57 | ||
|
|
c97d67e4fc | ||
|
|
e3265ace4b | ||
|
|
2e3afcbe45 | ||
|
|
236ac800d6 | ||
|
|
d395c47d4d | ||
|
|
178944b3da | ||
|
|
61457cbba8 | ||
|
|
a74d4ab0c6 | ||
|
|
3f561f004a | ||
|
|
26a3d1b3be | ||
|
|
a4e619fba2 | ||
|
|
f6230e76fd | ||
|
|
5a7f35c6ab | ||
|
|
1c5c3eba74 | ||
|
|
59569a9bfc | ||
|
|
9c7e5ae8dd | ||
|
|
bd0049ef57 | ||
|
|
f936f443ea | ||
|
|
4d85237f44 | ||
|
|
1cf8a96c3d | ||
|
|
99242384f5 | ||
|
|
aaed05bef8 | ||
|
|
857e3fb4a1 | ||
|
|
c0ae7f42d9 | ||
|
|
37605ca2b8 | ||
|
|
89ecbda923 | ||
|
|
dc24cabcbf | ||
|
|
ae2d1a438c | ||
|
|
b85917140d | ||
|
|
4373e81508 | ||
|
|
d1293259e7 | ||
|
|
381304867b | ||
|
|
1b4b5752aa | ||
|
|
94b3ae3715 | ||
|
|
b478099a2b | ||
|
|
26d8419678 | ||
|
|
1eb2cbf275 | ||
|
|
e1f3c3ad1c | ||
|
|
98c3787ed4 | ||
|
|
97fcce87c4 | ||
|
|
1536996a02 | ||
|
|
542bc66f86 | ||
|
|
206d1639b5 | ||
|
|
3a9ae627db | ||
|
|
831b5a8ba3 | ||
|
|
9b4f423afa | ||
|
|
e7ce5bcdb6 | ||
|
|
73bbb0d3dd | ||
|
|
1e5d2ef239 | ||
|
|
07b25b78ee | ||
|
|
e67518d0f0 | ||
|
|
c1b7c46eca | ||
|
|
d2d43aea2e | ||
|
|
1fa83a2d84 | ||
|
|
6d1f698882 | ||
|
|
3c4a34daaa | ||
|
|
590b487711 | ||
|
|
7344c934a4 | ||
|
|
9a67bc0f49 | ||
|
|
cbc5dc79a0 | ||
|
|
1fcde1cd4b | ||
|
|
a16e398feb | ||
|
|
25ca71a97a | ||
|
|
23618de189 | ||
|
|
43455723f6 | ||
|
|
d646240c63 | ||
|
|
d60ee9307d | ||
|
|
c2915d909d | ||
|
|
3187969200 | ||
|
|
30f18cd947 | ||
|
|
181a1d2f1a | ||
|
|
3404d267f7 | ||
|
|
0af6b92e9f | ||
|
|
c87a2bff23 | ||
|
|
32a42cf9ec | ||
|
|
e2189ea877 | ||
|
|
fe31d692f2 | ||
|
|
84c2ac918b | ||
|
|
6cf97aa2fc | ||
|
|
b58a561863 | ||
|
|
06c13804a4 | ||
|
|
bf8e88f76b | ||
|
|
84528b5251 | ||
|
|
f76428b668 | ||
|
|
d8abdf1a2e | ||
|
|
f9f83660f5 | ||
|
|
ddfd97c395 | ||
|
|
f658ba29c9 | ||
|
|
57cf22af43 | ||
|
|
eaea379532 | ||
|
|
2ac3829b6f | ||
|
|
29e3cf4a79 | ||
|
|
d84e2e4e23 | ||
|
|
02b8d39791 | ||
|
|
f7a74edc6e | ||
|
|
8feaf0795f | ||
|
|
87a857220f | ||
|
|
403550af5f | ||
|
|
d6ffdddfc6 | ||
|
|
7663df024c | ||
|
|
704c735bc5 | ||
|
|
ec72e9f7d7 | ||
|
|
f95ada602c | ||
|
|
7c10272d03 | ||
|
|
3c3c64a11a | ||
|
|
722c16e925 | ||
|
|
4a799cf45f | ||
|
|
16acf2d46c | ||
|
|
413c14a1dc | ||
|
|
826f37e4b9 | ||
|
|
39654e498c | ||
|
|
e3432ced3c | ||
|
|
917ae1daab | ||
|
|
a09086bfc2 | ||
|
|
2ac38e6639 | ||
|
|
3dc54790cc | ||
|
|
ce780c5175 | ||
|
|
35a76b4a43 | ||
|
|
999f990870 | ||
|
|
e050a62b0c | ||
|
|
1cd278afcf | ||
|
|
208089a5cd | ||
|
|
0c66c256b3 | ||
|
|
66e8154243 | ||
|
|
ff4db64802 | ||
|
|
4c29790b6c | ||
|
|
3efc4807cf | ||
|
|
a3b543cdd2 | ||
|
|
a12eedfa93 | ||
|
|
a97125d733 | ||
|
|
7b5d2fc053 | ||
|
|
0e587c5b3a | ||
|
|
6ea9a07a3d | ||
|
|
090a47b17f | ||
|
|
f79b326010 | ||
|
|
4c6aa708c1 | ||
|
|
7ac66a298b | ||
|
|
9e1baa9521 | ||
|
|
c1870b4ef8 | ||
|
|
a9b068c89e | ||
|
|
dab7d5030e | ||
|
|
120a314a96 | ||
|
|
bd4305dd13 | ||
|
|
d8b7214a2b | ||
|
|
95122ffcf8 | ||
|
|
3b1b0bc92f | ||
|
|
49f789c404 | ||
|
|
9cf447d422 | ||
|
|
1dc10b57c6 | ||
|
|
3d11422191 | ||
|
|
fa0fc14c25 | ||
|
|
22cdb7e721 | ||
|
|
d215657641 | ||
|
|
df33b9388f | ||
|
|
ac53f16098 | ||
|
|
36d6342e77 | ||
|
|
72e7822616 | ||
|
|
ea8f990172 | ||
|
|
8003ade509 | ||
|
|
3795232b2c | ||
|
|
dda26098f4 | ||
|
|
ae042d98f4 | ||
|
|
649faa3371 | ||
|
|
cea0c3f628 | ||
|
|
dce2ebbafb | ||
|
|
c80d703acf | ||
|
|
f0418b8d2a | ||
|
|
f363ae0f1e | ||
|
|
24d3801316 | ||
|
|
2a3f2c6dbd | ||
|
|
318a6e2924 | ||
|
|
800311fed8 | ||
|
|
e49d45bf4f | ||
|
|
356bd9c635 | ||
|
|
b400ea6d33 | ||
|
|
7352f9094a | ||
|
|
c90e8bd356 | ||
|
|
9f79073d14 | ||
|
|
249f8a4673 | ||
|
|
fe8ec70159 | ||
|
|
1f5375fed2 | ||
|
|
804d47df7b | ||
|
|
c6dfc1ceb2 | ||
|
|
b3727f6821 | ||
|
|
bc538da8ea | ||
|
|
465e25adff | ||
|
|
809fc513ea | ||
|
|
ea101baf0d | ||
|
|
19f88d6e6d | ||
|
|
7499d05bbc | ||
|
|
de2e3fc070 | ||
|
|
9b85293349 | ||
|
|
bee34c064b | ||
|
|
7ba64ce136 | ||
|
|
d80d2c5cda | ||
|
|
2419b93682 | ||
|
|
740b3f0c49 | ||
|
|
0925379a6d | ||
|
|
0c89087579 | ||
|
|
0565082309 | ||
|
|
cf7fcb07a1 | ||
|
|
ad99d21cdf | ||
|
|
3ad4da4f49 | ||
|
|
62238e0f81 | ||
|
|
2928f157fa | ||
|
|
cacb64b2ca | ||
|
|
1f65c4723d | ||
|
|
c2d4923706 | ||
|
|
5ea07389b4 | ||
|
|
8c56bb51ed | ||
|
|
7c8057dbc6 | ||
|
|
841b9ae668 | ||
|
|
f847a39eaa | ||
|
|
f2eafb99d4 | ||
|
|
847becccfe | ||
|
|
536c2483b0 | ||
|
|
af80228bcd | ||
|
|
334b086efb | ||
|
|
bddce9872f | ||
|
|
3d4e544573 | ||
|
|
7fe68a1c4a | ||
|
|
8693b6ce81 | ||
|
|
200b3f7092 | ||
|
|
69afbde352 | ||
|
|
052fb65518 | ||
|
|
c2ec6978c4 | ||
|
|
941e25d7a7 | ||
|
|
0bddb07ba0 | ||
|
|
869697352d | ||
|
|
9757fdb16b | ||
|
|
4d718177b6 | ||
|
|
2dff30f2cd | ||
|
|
8b5128510b | ||
|
|
1efa8d030b | ||
|
|
c855a107ab | ||
|
|
a379279956 | ||
|
|
3fb38ae682 | ||
|
|
ee29d4289c | ||
|
|
eeccc8490d | ||
|
|
2ae735db74 | ||
|
|
248d6c57c9 | ||
|
|
5eba9bfc14 | ||
|
|
7bc14a526d | ||
|
|
769521c978 | ||
|
|
bc71ef6a18 | ||
|
|
422ab9b835 | ||
|
|
410f9b1acf | ||
|
|
f6b3cb4b53 | ||
|
|
6a72aab106 | ||
|
|
f3695e18ac | ||
|
|
f64846c690 | ||
|
|
79bb27d2a7 | ||
|
|
f0dba0ca0e | ||
|
|
b70627bd3f | ||
|
|
91b894958c | ||
|
|
ef10e83f1c | ||
|
|
e958cb665e | ||
|
|
8413ec92d5 | ||
|
|
92d77b610d | ||
|
|
42f445b1d2 | ||
|
|
ca886f4c0c | ||
|
|
531ac8fc50 | ||
|
|
628c8f5ff1 | ||
|
|
b63037caf3 | ||
|
|
19127cb09d | ||
|
|
004e0a4265 | ||
|
|
9bbc23d550 | ||
|
|
77cea9f74e | ||
|
|
0bc57c145a | ||
|
|
92545309e1 | ||
|
|
bdc2fe8e69 | ||
|
|
1d3c5c85ec | ||
|
|
cd14da0454 | ||
|
|
73ccaac656 | ||
|
|
b19df1be9f | ||
|
|
f10bfe8e8d | ||
|
|
3418fdf822 | ||
|
|
f31098bf15 | ||
|
|
27ec3db33d | ||
|
|
8b618df712 | ||
|
|
a87d428563 | ||
|
|
d778138f92 | ||
|
|
1197b1c59a | ||
|
|
e5cf4e8cea | ||
|
|
4bbfb1852f | ||
|
|
d88ecc8505 | ||
|
|
6d1ef8257f | ||
|
|
5167eabb5c | ||
|
|
c8f08f61e5 | ||
|
|
9ed4822a93 | ||
|
|
0db2fba9fa | ||
|
|
117ee3d7bc | ||
|
|
c30261f9b8 | ||
|
|
5bd0fcf866 | ||
|
|
c888a09a40 | ||
|
|
c88818aa55 | ||
|
|
bafbdc8a5a | ||
|
|
a04c9f144a | ||
|
|
3301d91ae7 | ||
|
|
6c4ed98ad4 | ||
|
|
273894b734 | ||
|
|
2fd3b13d86 | ||
|
|
e3ec921e3d | ||
|
|
8e4ce46891 | ||
|
|
9c50f45866 | ||
|
|
2b3733a752 | ||
|
|
ae709d210e | ||
|
|
8f94bceaf2 | ||
|
|
36fbb71e1d | ||
|
|
464a1fe51c | ||
|
|
4067caa144 | ||
|
|
c576b0f65c | ||
|
|
6156ad254f | ||
|
|
3b0465aafe | ||
|
|
d7da7a3fd1 | ||
|
|
3fb515c73b | ||
|
|
8ef5fbc84b | ||
|
|
51f1c281e7 | ||
|
|
d93e9170ad | ||
|
|
418d4d5019 | ||
|
|
bf6c569c01 | ||
|
|
6e1a994971 | ||
|
|
0ed44d4afb | ||
|
|
92cc1c7718 | ||
|
|
b4347ff53d | ||
|
|
3e2b4418ef | ||
|
|
691264c2c3 | ||
|
|
a21b2acf6f | ||
|
|
54a3052fdb | ||
|
|
33fdd2969a | ||
|
|
2f146c9648 | ||
|
|
d7ead4cc3d | ||
|
|
f5b275d0d1 | ||
|
|
5b18502378 | ||
|
|
792fa2e9ca | ||
|
|
c1f4400441 | ||
|
|
18e813b3ed | ||
|
|
5bc66f4c76 | ||
|
|
df7d5e9c8e | ||
|
|
a9c68d2942 | ||
|
|
541afd807e | ||
|
|
598a391c12 | ||
|
|
a9c536322e | ||
|
|
2635c1798d | ||
|
|
92f1596336 | ||
|
|
9d41132be6 | ||
|
|
5c367ef940 | ||
|
|
423d38c94e | ||
|
|
4ad20fd7d6 | ||
|
|
7cff83168f | ||
|
|
7ea109aaf8 | ||
|
|
beb29502a7 | ||
|
|
1fed0a1264 | ||
|
|
d2182ad49c | ||
|
|
bb01e2a4d4 | ||
|
|
b46c0bb05d | ||
|
|
192ef621f6 | ||
|
|
4a43abc905 | ||
|
|
07dfb2e9df | ||
|
|
2f8beff993 | ||
|
|
72ec9a2b70 | ||
|
|
1ce6979910 | ||
|
|
c0794aff76 | ||
|
|
371ed047c6 | ||
|
|
b4cc04f32b | ||
|
|
42596623a9 | ||
|
|
345c809a23 | ||
|
|
fbe169f06d | ||
|
|
4f5fc25195 | ||
|
|
e1f3fb6ac0 | ||
|
|
9915d3c9ac | ||
|
|
0bbed86241 | ||
|
|
5076d03143 | ||
|
|
52b5ef7647 | ||
|
|
e9f8877dec | ||
|
|
ed87267633 | ||
|
|
e4d469a408 | ||
|
|
5c13e8d9aa | ||
|
|
7afdd4cbf5 | ||
|
|
c155317a6b | ||
|
|
a70513985a | ||
|
|
ffc2324dd5 | ||
|
|
9ef975d467 | ||
|
|
fa9c88c7be | ||
|
|
2d6b9b775e | ||
|
|
8a48371aec | ||
|
|
c891a0fa26 | ||
|
|
99728fd237 | ||
|
|
b76c65441b | ||
|
|
4ca8ec005d | ||
|
|
db3c1b22ce | ||
|
|
b0525bf06b | ||
|
|
d884731691 | ||
|
|
a4e4d089f7 | ||
|
|
0b31ee8562 | ||
|
|
aaf8969af2 | ||
|
|
f7c34aa572 | ||
|
|
16e4c11961 | ||
|
|
116003f189 | ||
|
|
6c87de30dd | ||
|
|
60b2601d8c | ||
|
|
ca37bda739 | ||
|
|
c4698212ce | ||
|
|
0e0d4031c9 | ||
|
|
be1f885341 | ||
|
|
a6c8eedfd4 | ||
|
|
32f9600d51 | ||
|
|
03f99c88ae | ||
|
|
47882191bc | ||
|
|
d7962ae659 | ||
|
|
cbd17b0290 | ||
|
|
839325ab70 | ||
|
|
13dc4304fc | ||
|
|
c49a937fb8 | ||
|
|
36d3905d0e | ||
|
|
9ff748dfd5 | ||
|
|
40878794cf | ||
|
|
a17a69c93a | ||
|
|
469ca98b8a | ||
|
|
330cc1cd3a | ||
|
|
5c61819b25 | ||
|
|
2c31c26cb9 | ||
|
|
a4f490b607 | ||
|
|
36007330e9 | ||
|
|
fba890af81 | ||
|
|
619fee0821 | ||
|
|
4f7d3f4e4e | ||
|
|
ac1f1d2de4 | ||
|
|
5856edd962 | ||
|
|
ac463d081e | ||
|
|
dbd1a872c4 | ||
|
|
ff600d652d | ||
|
|
8a512a13a4 | ||
|
|
a9f1a25da4 | ||
|
|
0fd05f2c36 | ||
|
|
aa0754a53f | ||
|
|
d31d0aa735 | ||
|
|
c92b26928c | ||
|
|
70f5f1be30 | ||
|
|
4ae769506e | ||
|
|
73c6b67830 | ||
|
|
f05963c6ea | ||
|
|
d99b687989 | ||
|
|
d57e4bd077 | ||
|
|
e5c2a18bd6 | ||
|
|
796a2b44c8 | ||
|
|
5e67cf8292 | ||
|
|
5e4880f1d1 | ||
|
|
ea161d83fa | ||
|
|
0f28ca779d | ||
|
|
7711af1214 | ||
|
|
9817eb303b | ||
|
|
21d782b4be | ||
|
|
f14d816e6b | ||
|
|
025ee4df18 | ||
|
|
1b2870a1b2 | ||
|
|
03760521ac | ||
|
|
e4d8983280 | ||
|
|
64431e3661 | ||
|
|
ace56ece7e | ||
|
|
135724fab5 | ||
|
|
5d72b7ce98 | ||
|
|
6697a44259 | ||
|
|
85165850c1 | ||
|
|
3bd24d80c0 | ||
|
|
aab31719d4 | ||
|
|
1ffc84663a | ||
|
|
9f703cf64e | ||
|
|
44b1e2a4e5 | ||
|
|
3e7fa3e440 | ||
|
|
20154d8411 | ||
|
|
26b2a2278e | ||
|
|
886901b48e | ||
|
|
a817967aa0 | ||
|
|
52bbfdb9f4 | ||
|
|
8c74096a7f | ||
|
|
153362393b | ||
|
|
3185d0b38b | ||
|
|
0be8b711ac | ||
|
|
9e60002210 | ||
|
|
fe169f267d | ||
|
|
26c09c3993 | ||
|
|
49f516685f | ||
|
|
89ca710932 | ||
|
|
f1d01341cf | ||
|
|
c30beda09d | ||
|
|
4b4d9da7b1 | ||
|
|
32a94fbb37 | ||
|
|
a589857582 | ||
|
|
f9c20c4a91 | ||
|
|
886d07a4c3 | ||
|
|
b1ca421e52 | ||
|
|
1cb583a3ba | ||
|
|
4683318351 | ||
|
|
e17a4756e7 | ||
|
|
7622f7c550 | ||
|
|
e25332ed27 | ||
|
|
1f270a5231 | ||
|
|
cc32286389 | ||
|
|
d78b2f2fc0 | ||
|
|
a406bc94a8 | ||
|
|
765a743d5b | ||
|
|
40a0d3a3e4 | ||
|
|
b612791fc8 | ||
|
|
c4cc51cf8e | ||
|
|
3494d388bb | ||
|
|
88b887c579 | ||
|
|
061f3ce78f | ||
|
|
944d2475dd | ||
|
|
1d1e57a265 | ||
|
|
522b874f54 | ||
|
|
dea2cfad1c | ||
|
|
1c8369d6fd | ||
|
|
98d8d65687 | ||
|
|
c5c9f5a447 | ||
|
|
c42a3ac6e0 | ||
|
|
be60c4f865 | ||
|
|
fd9bb6f36a | ||
|
|
27118a0f7a | ||
|
|
665d4af5a2 | ||
|
|
1fd2aa17d5 | ||
|
|
bd8db95098 | ||
|
|
fcb3f64d9b | ||
|
|
ee36ab94b6 | ||
|
|
ac86437b35 | ||
|
|
30d8d9f3fe | ||
|
|
f2ace5cd3d | ||
|
|
cc1a71f246 | ||
|
|
b3d8eaee7d | ||
|
|
c70861d7d3 | ||
|
|
4cfa915aec | ||
|
|
2bf31add95 | ||
|
|
61526dea82 | ||
|
|
5c719c27b0 | ||
|
|
342207375e | ||
|
|
ce5b26b0d0 | ||
|
|
6056bfbc49 | ||
|
|
4e925b0385 | ||
|
|
edff4117f5 | ||
|
|
64f2d62605 | ||
|
|
1efe09eeca | ||
|
|
2375176d72 | ||
|
|
3bfc7da93a | ||
|
|
34b4e90d75 | ||
|
|
48ab353521 | ||
|
|
73f872bf6e | ||
|
|
f04c8be999 | ||
|
|
d6eb49db68 | ||
|
|
c03ada1e1c | ||
|
|
9ce3d6182b | ||
|
|
ff3f60cc68 | ||
|
|
89aa4bfab3 | ||
|
|
746e60b517 | ||
|
|
8634cc561f | ||
|
|
d384f0896a | ||
|
|
9f5cc344ad | ||
|
|
45bebdff0c | ||
|
|
59ef9e132a | ||
|
|
af8e9c2466 | ||
|
|
e11163f483 | ||
|
|
d3ff20484d | ||
|
|
76cbbb2bfe | ||
|
|
c51ad4aaa6 | ||
|
|
6d5e56f6bf | ||
|
|
251569e81c | ||
|
|
be2759c354 | ||
|
|
69ee8f3630 | ||
|
|
610991a222 | ||
|
|
f08a0eaffd | ||
|
|
a0baa659ec | ||
|
|
536a19b373 | ||
|
|
27a2d7f642 | ||
|
|
fc29cec8c8 | ||
|
|
5fe19d75a6 | ||
|
|
3252489971 | ||
|
|
f90af4784e | ||
|
|
a2f11fb829 | ||
|
|
b6e6d308a9 | ||
|
|
dd04888106 | ||
|
|
0c397a414e | ||
|
|
2a62001048 | ||
|
|
3132e5dd5b | ||
|
|
8843a12333 | ||
|
|
b2e97ac73b | ||
|
|
11c440209a | ||
|
|
400adeed1c | ||
|
|
abcdc7253e | ||
|
|
b6b8b9440f | ||
|
|
c9a64b8e96 | ||
|
|
327795855a | ||
|
|
b3c5494e74 | ||
|
|
9fc4a247aa | ||
|
|
013cd0a9a8 | ||
|
|
05a9da4eb8 | ||
|
|
81d853ac04 | ||
|
|
af8dfa685a | ||
|
|
946a360652 | ||
|
|
071fe9a910 | ||
|
|
ccb4e87002 | ||
|
|
cd9adb166a | ||
|
|
4f95f2c842 | ||
|
|
7e6771d550 | ||
|
|
0b025fd0ae | ||
|
|
9445204ffc | ||
|
|
0b69923ad7 | ||
|
|
00eb90bcc6 | ||
|
|
8478967c04 | ||
|
|
08f4b5bbe6 | ||
|
|
828a5e7f46 | ||
|
|
2e6151fe25 | ||
|
|
c5fad910ba | ||
|
|
8d7c672b94 | ||
|
|
67de9ba209 | ||
|
|
d3486341f3 | ||
|
|
ac12412ec3 | ||
|
|
73e0a4d35a | ||
|
|
1e09e9e3f8 | ||
|
|
d97265f2f2 | ||
|
|
cd55c4077e | ||
|
|
0a9d5b73d8 | ||
|
|
e5e1de83a1 | ||
|
|
359ab4233b | ||
|
|
715f991d0d | ||
|
|
e00b18bf40 | ||
|
|
77869fd273 | ||
|
|
1f4de4af80 | ||
|
|
6d34e04138 | ||
|
|
1761ccffd0 | ||
|
|
4de53b29f0 | ||
|
|
08c02b7473 | ||
|
|
a648e888c5 | ||
|
|
af94789b7f | ||
|
|
c4520a0ac2 | ||
|
|
75bec0a132 | ||
|
|
b680878558 | ||
|
|
3f95ca2869 | ||
|
|
003d7ac606 | ||
|
|
b61267d3bd | ||
|
|
a6728853db | ||
|
|
d95c46e1b8 | ||
|
|
d3a18c55d3 | ||
|
|
8bffadeb14 | ||
|
|
4abb037081 | ||
|
|
07d6cebba4 | ||
|
|
abd929070e | ||
|
|
ea937a410b | ||
|
|
64159bab6f | ||
|
|
164e6656e8 | ||
|
|
0bac699cbf | ||
|
|
2030ffcea3 | ||
|
|
6433eff8ec | ||
|
|
6c51f7c6bc | ||
|
|
0c82e0614c | ||
|
|
acd30aaf08 | ||
|
|
ce2f75d43d | ||
|
|
3f3875cc0c | ||
|
|
e939218a65 | ||
|
|
d929a8b140 | ||
|
|
3e1aef4a58 | ||
|
|
93dad0bf44 | ||
|
|
d5259e8bfb | ||
|
|
440d7fe2b8 | ||
|
|
0e666f07e9 | ||
|
|
1b542e936b | ||
|
|
13487bbeed | ||
|
|
7a71716c7f | ||
|
|
e72baa9dd4 | ||
|
|
4c9f188b0c | ||
|
|
e78f90bc62 | ||
|
|
19d5e8457f | ||
|
|
0fb0837561 | ||
|
|
6cff21f47b | ||
|
|
477408676d | ||
|
|
97b675bc9f | ||
|
|
65dab56ad0 | ||
|
|
faba97a6f5 | ||
|
|
c10a2e1133 | ||
|
|
83d13cc153 | ||
|
|
bdbdd990a7 | ||
|
|
70e4d80965 | ||
|
|
6ec8b15a34 | ||
|
|
ee21afee61 | ||
|
|
08a08855aa | ||
|
|
95ee7ba2f8 | ||
|
|
cde55f37f3 | ||
|
|
5b147d4ab2 | ||
|
|
058ef0c4c5 | ||
|
|
bb48bd22a3 | ||
|
|
0aecd7068f | ||
|
|
0d4a837be6 | ||
|
|
ac70e6504a | ||
|
|
a64f19a686 | ||
|
|
9a12f8ecab | ||
|
|
37d6e5aa3d | ||
|
|
7cbf5ff74e | ||
|
|
b6ec9728d8 | ||
|
|
b5a167df05 | ||
|
|
f01409ab2c | ||
|
|
a96778a94d | ||
|
|
e9e898da79 | ||
|
|
a8d73f6172 | ||
|
|
84a1d9997b | ||
|
|
5aa489d769 | ||
|
|
fd520f2ea8 | ||
|
|
a1a7cb747b | ||
|
|
3d79e77f47 | ||
|
|
f6c6ecd494 | ||
|
|
52cca9532b | ||
|
|
d412bcc718 | ||
|
|
6cb93e51fa | ||
|
|
84bc9205a9 | ||
|
|
92aea19abd | ||
|
|
5dc94fdf0e | ||
|
|
2089450a1d | ||
|
|
eb92fe944c | ||
|
|
3f426f63a1 | ||
|
|
8c3a96db7f | ||
|
|
edd9142f64 | ||
|
|
424b83ddeb | ||
|
|
610e844747 | ||
|
|
a6fbe153b5 | ||
|
|
0c1108b92d | ||
|
|
684b84690c | ||
|
|
df808f5690 | ||
|
|
d960623dac | ||
|
|
d30c95c58c | ||
|
|
8c1139c151 | ||
|
|
3a6adc5503 | ||
|
|
dbc1556381 | ||
|
|
bd85ed3577 | ||
|
|
1340e43f02 | ||
|
|
aee891cd7e | ||
|
|
9f39c1d305 | ||
|
|
43c23059ba | ||
|
|
12c1caf54d | ||
|
|
3848ba311e | ||
|
|
dcb6cbf5fa | ||
|
|
f70e33a3ea | ||
|
|
857d6f87a0 | ||
|
|
d3444621b9 | ||
|
|
05bfc51a20 | ||
|
|
851c593695 | ||
|
|
8ed711ad34 | ||
|
|
e8eaf60c2c | ||
|
|
df480b67c0 | ||
|
|
31e7316854 | ||
|
|
b183588046 | ||
|
|
2620769b19 | ||
|
|
9f4184974e | ||
|
|
e586debb58 | ||
|
|
eccaf83e7d | ||
|
|
a9a6227b8f | ||
|
|
f16251d046 | ||
|
|
27c508caa0 | ||
|
|
aa10845ef0 | ||
|
|
fe4c836cd2 | ||
|
|
a34afb1a81 | ||
|
|
9ee58451b5 | ||
|
|
faa8ca0b34 | ||
|
|
e084f66354 | ||
|
|
c68887f5f5 | ||
|
|
43862a4b7b | ||
|
|
036d672e7c | ||
|
|
abbbae011f | ||
|
|
bbab6a0d68 | ||
|
|
2e492dfc39 | ||
|
|
3d5606cc7b | ||
|
|
553d059113 | ||
|
|
990345cda0 | ||
|
|
1cbf075036 | ||
|
|
6e518df398 | ||
|
|
8e1af41938 | ||
|
|
da2faaeff4 | ||
|
|
66af4ef831 | ||
|
|
cc0fdb302b | ||
|
|
fd1f686469 | ||
|
|
a45a8a6011 | ||
|
|
a40c362764 | ||
|
|
b3c372a803 | ||
|
|
0ffe6051b6 | ||
|
|
3613cb704e | ||
|
|
0733f7a69a | ||
|
|
d514944657 | ||
|
|
5172aaafdb | ||
|
|
d8ec34aa6f | ||
|
|
0257acce1d | ||
|
|
f7746a0da6 | ||
|
|
52605f6a4a | ||
|
|
d865a8ba0c | ||
|
|
2869337f52 | ||
|
|
083f28c89d | ||
|
|
bd78dff68f | ||
|
|
db291eb9c4 | ||
|
|
f155244997 | ||
|
|
fa8d2f0a25 | ||
|
|
f3b0259884 | ||
|
|
258a9c046e | ||
|
|
97bda7a949 | ||
|
|
6b306860ec | ||
|
|
697c41b0d0 | ||
|
|
ee8ec490d7 | ||
|
|
c1674b568a | ||
|
|
2c7d75996e | ||
|
|
579c08a9e8 | ||
|
|
2ba6e616af | ||
|
|
5d32321aa2 | ||
|
|
ccd50fa98b | ||
|
|
69c9cb1244 | ||
|
|
35e1fca22d | ||
|
|
039dfb63ea | ||
|
|
cf53d03104 | ||
|
|
d4e72f07f9 | ||
|
|
08a6af547c | ||
|
|
dd8a526a30 | ||
|
|
298a6b48ad | ||
|
|
08d305ebfa | ||
|
|
e4f0463299 | ||
|
|
b586182160 | ||
|
|
4051da7ee1 | ||
|
|
1705cc21b4 | ||
|
|
4a3e0b244c | ||
|
|
3855fa3568 | ||
|
|
70b0e7d6c0 | ||
|
|
e40f3cb34d | ||
|
|
434deb92f8 | ||
|
|
d962905799 | ||
|
|
bb10ce78a0 | ||
|
|
0670847700 | ||
|
|
c539acb823 | ||
|
|
f233c2683e | ||
|
|
e5a7f5844c | ||
|
|
db1839c402 | ||
|
|
24b54a0937 | ||
|
|
c3f7640c4d | ||
|
|
dab74b7bba | ||
|
|
c0e9bb223e | ||
|
|
0630658d4d | ||
|
|
3094850579 | ||
|
|
7b29e2e603 | ||
|
|
53784e154d | ||
|
|
20e3bbf288 | ||
|
|
43d016c288 | ||
|
|
320cc45fc8 | ||
|
|
a1831ab338 | ||
|
|
6f57f56d1d | ||
|
|
41062dfc47 | ||
|
|
e6628d4569 | ||
|
|
7c389dbf8e | ||
|
|
651f7cc7cd | ||
|
|
bd70662314 | ||
|
|
9a45a84663 | ||
|
|
5b12d0e3f6 | ||
|
|
d9e1121796 | ||
|
|
b0f37ca217 | ||
|
|
7459f7a9fa | ||
|
|
3f3f2f2c18 | ||
|
|
42f2b0a8e3 | ||
|
|
8326dc1dc7 | ||
|
|
cc68c5cfcb | ||
|
|
9e46cc2b0b | ||
|
|
2fc827fcd7 | ||
|
|
8108bdc91b | ||
|
|
345f6fc2af | ||
|
|
af06cb5cdd | ||
|
|
ba74b333cd | ||
|
|
895d282cc8 | ||
|
|
33831ee7cc | ||
|
|
8aaea65a72 | ||
|
|
e20ba2c0a0 | ||
|
|
e276950caa | ||
|
|
a17663655b | ||
|
|
9bb5a69b56 | ||
|
|
b9afd98189 | ||
|
|
89da789a99 | ||
|
|
f78f8006d6 | ||
|
|
46c79b4213 | ||
|
|
e4fb8c20ac | ||
|
|
5a8762edb1 | ||
|
|
6285c76935 | ||
|
|
77942b632f | ||
|
|
29261c34d5 | ||
|
|
2152e5f731 | ||
|
|
b659833f71 | ||
|
|
f7d626d8c6 | ||
|
|
f1c8ace081 | ||
|
|
655015d33b | ||
|
|
ec24a92987 | ||
|
|
04237c6118 | ||
|
|
86f13e05d2 | ||
|
|
9a6146e1ba | ||
|
|
0a8ce34252 | ||
|
|
18529f47e7 | ||
|
|
98215be0fc | ||
|
|
f2698ef38d | ||
|
|
215ee66a02 | ||
|
|
e055657d13 | ||
|
|
ec7f47dbe7 | ||
|
|
e3e7e4d26a | ||
|
|
118d7d735a | ||
|
|
d7a8534354 | ||
|
|
9fe86bf72a | ||
|
|
1d76f89428 | ||
|
|
fc26af5a89 | ||
|
|
7f85a8f2e1 | ||
|
|
e3ff8029de | ||
|
|
70f5bdd339 | ||
|
|
2ecc36ab12 | ||
|
|
a4c14c6be1 | ||
|
|
6cbda005e8 | ||
|
|
8ea99648f0 | ||
|
|
58a5d9b0e1 | ||
|
|
b969f2eac6 | ||
|
|
5c295e9080 | ||
|
|
64a6c3c5bb | ||
|
|
22cc4c635a | ||
|
|
b5b96f1155 | ||
|
|
06b887cd93 | ||
|
|
c6ef376d15 | ||
|
|
6ef249195e | ||
|
|
f6e99cf748 | ||
|
|
6d39088777 | ||
|
|
e5cb026608 | ||
|
|
ce9dbc2b5f | ||
|
|
bfef677e93 | ||
|
|
0d18a1d19a | ||
|
|
91b3b7194d | ||
|
|
306d515e7f | ||
|
|
0a21e03eac | ||
|
|
e112704a63 | ||
|
|
c0bbf107c6 | ||
|
|
84a6876265 | ||
|
|
add9ffe5d9 | ||
|
|
c71f0b7e39 | ||
|
|
e934071716 | ||
|
|
5693cddc3a | ||
|
|
561d27be0a | ||
|
|
2e19a2a99c | ||
|
|
4055e421bc | ||
|
|
792255b676 | ||
|
|
0ddb8d9644 | ||
|
|
5c29555ae7 | ||
|
|
cf5e6bde9a | ||
|
|
515ddbb28d | ||
|
|
18720e3693 | ||
|
|
6b21664dff | ||
|
|
ca92f90f6d | ||
|
|
8a1ab40f3c | ||
|
|
f738822cce | ||
|
|
2ef900443b | ||
|
|
917c6b7c54 | ||
|
|
966db8af9f | ||
|
|
07b26f03a7 | ||
|
|
d685997040 | ||
|
|
fce6807a3b | ||
|
|
e91b428bd5 | ||
|
|
6581466239 | ||
|
|
380a5bbadd | ||
|
|
4da83ff5f8 | ||
|
|
d5cf8848fa | ||
|
|
4ea6f9dd6a | ||
|
|
d8027b002d | ||
|
|
623cda83d5 | ||
|
|
99b3e4493b | ||
|
|
025a201a7d | ||
|
|
3243555181 | ||
|
|
867eae442b | ||
|
|
e8e80a7ac8 | ||
|
|
480c88a73e | ||
|
|
6f34a42034 | ||
|
|
312421ed28 | ||
|
|
93e81fdc01 | ||
|
|
2d7f0f907b | ||
|
|
6aca67efa6 | ||
|
|
bab2b9f528 | ||
|
|
07886140fb | ||
|
|
90341ee27a | ||
|
|
f78da4f637 | ||
|
|
d3b8ffe328 | ||
|
|
e0b25527b3 | ||
|
|
56798a76c9 | ||
|
|
2faab75bd8 | ||
|
|
d4d7c5b35e | ||
|
|
bf07df8f7b | ||
|
|
3622103a90 | ||
|
|
31680b3849 | ||
|
|
aa7611fceb | ||
|
|
9f0a34455b | ||
|
|
da51f8af37 | ||
|
|
170853f2cc | ||
|
|
df4b625ac4 | ||
|
|
2d6ff3b74c | ||
|
|
a5b3b61052 | ||
|
|
1b1a56e77c | ||
|
|
e18dd07491 | ||
|
|
f94771730e | ||
|
|
cb24df8890 | ||
|
|
499d72d5d6 | ||
|
|
1bab2617b7 | ||
|
|
b5ed2daa81 | ||
|
|
c32cd456b6 | ||
|
|
0f9a71f07d | ||
|
|
a1711daead | ||
|
|
5b6031f41d | ||
|
|
c3787ab02f | ||
|
|
9d1a216a20 | ||
|
|
2603d08aa4 | ||
|
|
990c0afee2 | ||
|
|
1bfd401ed7 | ||
|
|
22414bab7c | ||
|
|
08222b0d5c | ||
|
|
d599d29514 | ||
|
|
e5120c7074 | ||
|
|
68f36bf172 | ||
|
|
ee025f9a13 | ||
|
|
68891d18f4 | ||
|
|
be4b7e7232 | ||
|
|
c3e03c17e8 | ||
|
|
1dc63294b4 | ||
|
|
63191515d3 | ||
|
|
f8b072f14a | ||
|
|
d09862522b | ||
|
|
ce5240223b | ||
|
|
5b8d2cb36a | ||
|
|
d9b35ca1ee | ||
|
|
e8433a1d80 | ||
|
|
092aac82ea | ||
|
|
9039178927 | ||
|
|
a8b2a627f8 | ||
|
|
da8b85decd | ||
|
|
73786bfd94 | ||
|
|
e06ea82f62 | ||
|
|
085732868c | ||
|
|
3f7c512c63 | ||
|
|
28d8b49de7 | ||
|
|
c79d04f29a | ||
|
|
d1ba022ce9 | ||
|
|
d36fb16b12 | ||
|
|
26e94d3ba6 | ||
|
|
d2cd5e7202 | ||
|
|
0bfd5cc292 | ||
|
|
deb25f8299 | ||
|
|
aab28fd3fd | ||
|
|
d9759305a3 | ||
|
|
e015e57dda | ||
|
|
44a2f1880d | ||
|
|
2e3617ad1c | ||
|
|
ae5e7d1e60 | ||
|
|
a31c3dc611 | ||
|
|
2ee3ec303d | ||
|
|
3d8a85a145 | ||
|
|
fc18153f02 | ||
|
|
77c1875b6b | ||
|
|
4388a84787 | ||
|
|
0d3a9c8b16 | ||
|
|
2ca05b5634 | ||
|
|
4569bca39d | ||
|
|
c401a609a4 | ||
|
|
07a71f1059 | ||
|
|
82f6f7d4d5 | ||
|
|
97f68a8247 | ||
|
|
1d9f867c75 | ||
|
|
ebd3c33051 | ||
|
|
00cec4faa0 | ||
|
|
690fa7bf78 | ||
|
|
ace6474157 | ||
|
|
974a017044 | ||
|
|
89917d4f75 | ||
|
|
f651364e00 | ||
|
|
2a8b43b7a3 | ||
|
|
aff991690d | ||
|
|
7f095947ab | ||
|
|
b18b2cff44 | ||
|
|
befd16238b | ||
|
|
20d3e540b0 | ||
|
|
cede2927c5 | ||
|
|
543aae0893 | ||
|
|
e9c55345b8 | ||
|
|
8c7dc08844 | ||
|
|
3ae20885c4 | ||
|
|
09042f1dbf | ||
|
|
6b5c52d2c8 | ||
|
|
e02e2c462b | ||
|
|
30a1ea7a4f | ||
|
|
51a2383574 | ||
|
|
7fbbb9b030 | ||
|
|
f2d88796ed | ||
|
|
1659ca88ca | ||
|
|
fd7ba65203 | ||
|
|
ae66502fd5 | ||
|
|
cc06fb6b40 | ||
|
|
a60c6af30e | ||
|
|
91e7cd6576 | ||
|
|
d717a68445 | ||
|
|
7f4dcf54a7 | ||
|
|
613238e0f9 | ||
|
|
72f7189aeb | ||
|
|
ea2616434c | ||
|
|
0261f25996 | ||
|
|
f5db79ce69 | ||
|
|
236a7c91d5 | ||
|
|
c6bae2116a | ||
|
|
05d42d9518 | ||
|
|
19a6236fa9 | ||
|
|
115dbc8b12 | ||
|
|
03084c3ce3 | ||
|
|
0260afa068 | ||
|
|
2f9044f53f | ||
|
|
f195e85fb8 | ||
|
|
65065343f9 | ||
|
|
50c8014977 | ||
|
|
911d0a4fae | ||
|
|
d1bb81e404 | ||
|
|
76526a0b2d | ||
|
|
a9020d92ca | ||
|
|
74be6e6c14 | ||
|
|
f203168542 | ||
|
|
3c03f5fd71 | ||
|
|
8cfc9d7793 | ||
|
|
ec5c500b8f | ||
|
|
8f7cbeb4ab | ||
|
|
fb32273477 | ||
|
|
1be49e8d8a | ||
|
|
b7edc01952 | ||
|
|
0470878ddf | ||
|
|
d07a65d553 | ||
|
|
58218f10da | ||
|
|
690eebb6a1 | ||
|
|
9bd9936781 | ||
|
|
bf62325329 | ||
|
|
a3775e0b57 | ||
|
|
03c0794b48 | ||
|
|
b5edcaf5ac | ||
|
|
3723a4e656 | ||
|
|
b332038722 | ||
|
|
33df8dd5eb | ||
|
|
7e982b327b | ||
|
|
91585464bd | ||
|
|
3c3f494034 | ||
|
|
87cbc6229a | ||
|
|
bd10364d27 | ||
|
|
36721dde56 | ||
|
|
6a74bd841a | ||
|
|
093b5451c3 | ||
|
|
896dff3fdc | ||
|
|
e8e23603c3 | ||
|
|
426fe0fe40 | ||
|
|
3e2ef43a0a | ||
|
|
a73b480b1f | ||
|
|
4d374dd587 | ||
|
|
3b97f8c396 | ||
|
|
e7a559327c | ||
|
|
ede35abe1a | ||
|
|
edc36f915d | ||
|
|
05a9d457aa | ||
|
|
cc7ce6bae9 | ||
|
|
57e5135346 | ||
|
|
74980f84d5 | ||
|
|
fa7f1cc21a | ||
|
|
8c90fb3c15 | ||
|
|
021a48c070 | ||
|
|
8be583083e | ||
|
|
8d15a7249a | ||
|
|
6259793f39 | ||
|
|
772faf8cd2 | ||
|
|
dd7141aeaa | ||
|
|
521a3f9341 | ||
|
|
452bdd7b4f | ||
|
|
37ebb5731d | ||
|
|
933ce64cce | ||
|
|
b8d0019ece | ||
|
|
4df3da0856 | ||
|
|
59f482e52e | ||
|
|
538ea82899 | ||
|
|
0b7fc67c3f | ||
|
|
7a92712f0b | ||
|
|
6ed9a8b336 | ||
|
|
24df4e78c7 | ||
|
|
a0abaf62b4 | ||
|
|
85ece1ec63 | ||
|
|
ba284aafe3 | ||
|
|
f25ab2f892 | ||
|
|
b026111ef8 | ||
|
|
9850f2e242 | ||
|
|
9313fa9193 | ||
|
|
f19c9183f0 | ||
|
|
5af3384b02 | ||
|
|
46a5512209 | ||
|
|
63c83e97c4 | ||
|
|
cf311fdeb1 | ||
|
|
04ef82bc70 | ||
|
|
7182ad2e9c | ||
|
|
072508e1c1 | ||
|
|
91101a7c40 | ||
|
|
d34fa71797 | ||
|
|
bee7bccc12 | ||
|
|
74eea096b1 | ||
|
|
02c028584b | ||
|
|
2a87d338b4 | ||
|
|
8759532964 | ||
|
|
f332db93ad | ||
|
|
6ffc687174 | ||
|
|
ba6079164c | ||
|
|
372ee945de | ||
|
|
0f0eed3de6 | ||
|
|
9eddb37c3e | ||
|
|
6a262ea604 | ||
|
|
94693f530e | ||
|
|
ee9eced602 | ||
|
|
58fa9b8182 | ||
|
|
89b377ab8f | ||
|
|
1d93fccbb3 | ||
|
|
f8786bcd47 | ||
|
|
8adf2043f7 | ||
|
|
8af3fe3db8 | ||
|
|
09c1db9afe | ||
|
|
2f738ff2b7 | ||
|
|
4429e36c3f | ||
|
|
0b8a3cfcea | ||
|
|
c3d43f6021 | ||
|
|
2a5badbc7c | ||
|
|
3c5f24f99d | ||
|
|
105212a667 | ||
|
|
f5decd2221 | ||
|
|
48bd65cd3b | ||
|
|
90046b11af | ||
|
|
6eede614f8 | ||
|
|
8cd9c5e8a3 | ||
|
|
0ef1019d1c | ||
|
|
f2a55e525b | ||
|
|
0f9eecd821 | ||
|
|
a45848cbe3 | ||
|
|
07af7d7f97 | ||
|
|
87186ca940 | ||
|
|
990bd7e291 | ||
|
|
c39aa35d36 | ||
|
|
3b187d75bb | ||
|
|
84694773a2 | ||
|
|
e8d9083f05 | ||
|
|
25ccebe617 | ||
|
|
c6d3d07b3c | ||
|
|
c6078f9314 | ||
|
|
db5462b79f | ||
|
|
3b8e6a2c3a | ||
|
|
eae7e79ecf | ||
|
|
5264873645 | ||
|
|
6009f097cd | ||
|
|
96265e5ac1 | ||
|
|
5ee33a35fc | ||
|
|
55f3c484f2 | ||
|
|
0bd678318c | ||
|
|
89ab049391 | ||
|
|
aa6e4c5b53 | ||
|
|
ec8a75be52 | ||
|
|
3ae7fe7f04 | ||
|
|
3e4b5830be | ||
|
|
8ea28dad68 | ||
|
|
17c1fc79e1 | ||
|
|
801eaaaf37 | ||
|
|
bfd72d7540 | ||
|
|
1341ef0c3e | ||
|
|
95bda0d4bb | ||
|
|
adf412ea74 | ||
|
|
a00a6734b3 | ||
|
|
e410eab336 | ||
|
|
1367357e68 | ||
|
|
4cb4b3bb19 | ||
|
|
ce92ab77ec | ||
|
|
e365fc60bc | ||
|
|
edb1343676 | ||
|
|
f1426103ff | ||
|
|
7a767327c0 | ||
|
|
e2f9139aa9 | ||
|
|
a80b504379 | ||
|
|
d73b3c44e3 | ||
|
|
c05f54c105 | ||
|
|
c917bafde5 | ||
|
|
b349106f08 | ||
|
|
1c91f345fc | ||
|
|
2b11afbb2d | ||
|
|
e9479dd24f | ||
|
|
d676412291 | ||
|
|
5fd5d032ef | ||
|
|
66ee09f656 | ||
|
|
8f57a55286 | ||
|
|
6498250114 | ||
|
|
bf7b9a7057 | ||
|
|
8de7099c0f | ||
|
|
c2b3c4919d | ||
|
|
90b5531041 | ||
|
|
e57de3268f | ||
|
|
b9e68834de | ||
|
|
5352514c2b | ||
|
|
aa58b3df35 | ||
|
|
dfe5a90b06 | ||
|
|
893486eb93 | ||
|
|
bdc684d2d1 | ||
|
|
80bc435df6 | ||
|
|
00cbfa45d7 | ||
|
|
5c18270826 | ||
|
|
aa86ef631b | ||
|
|
c67a2bd458 | ||
|
|
f796ec32a7 | ||
|
|
9ce43ea7a6 | ||
|
|
19702794e4 | ||
|
|
9015bfcac3 | ||
|
|
ad13ef9187 | ||
|
|
4a1b147a25 | ||
|
|
5970908a3f | ||
|
|
e3d37ca831 | ||
|
|
4c9afb8c80 | ||
|
|
253049ec5a | ||
|
|
c1d7f86542 | ||
|
|
aa44fba1e5 | ||
|
|
5dbf2b0ff5 | ||
|
|
8fcbaef064 | ||
|
|
08c3509823 | ||
|
|
d9961bc1a0 | ||
|
|
9fd8c1f546 | ||
|
|
d265102f31 | ||
|
|
063a50b14a | ||
|
|
ebb06b34b0 | ||
|
|
bbc3a75e4a | ||
|
|
86c5ef363e | ||
|
|
daefcab8f5 | ||
|
|
3d5f9beac5 | ||
|
|
0b01855bf4 | ||
|
|
cc78904582 | ||
|
|
306b4652eb | ||
|
|
721a629ba8 | ||
|
|
4f00aeeed7 | ||
|
|
88ca671965 | ||
|
|
7c3cfe1326 | ||
|
|
735f1007cf | ||
|
|
66df063e2a | ||
|
|
e7547905a3 | ||
|
|
206b01376d | ||
|
|
e50ef893e8 | ||
|
|
496cc470e2 | ||
|
|
b82516ba4d | ||
|
|
bbe7ff3c9b | ||
|
|
d33770f107 | ||
|
|
6523500d09 | ||
|
|
1a7a5fe6a8 | ||
|
|
9426c531e3 | ||
|
|
35a6977df8 | ||
|
|
c06280a1ea | ||
|
|
8b3ff137d1 | ||
|
|
0fa826f926 | ||
|
|
5c7b40248c | ||
|
|
7a3a8b0490 | ||
|
|
78d13749d5 | ||
|
|
1143a43d30 | ||
|
|
3cdf553142 | ||
|
|
f9569895b4 | ||
|
|
905b469515 | ||
|
|
aac3443b78 | ||
|
|
bd9b05463e | ||
|
|
bb82c6fa02 | ||
|
|
7232f9446a | ||
|
|
639f2b8e02 | ||
|
|
7d860542b1 | ||
|
|
7593dad6c8 | ||
|
|
ecc4834110 | ||
|
|
2e1adc6f9b | ||
|
|
18a0866735 | ||
|
|
4036fc44e3 | ||
|
|
9f7bbd6efc | ||
|
|
1ff11a09ed | ||
|
|
be3d4723ef | ||
|
|
190703d74b | ||
|
|
eac361d892 | ||
|
|
a772cf9910 | ||
|
|
77eab9e6f8 | ||
|
|
c4fc168369 | ||
|
|
41409fb5c1 | ||
|
|
71eb88eb7b | ||
|
|
dcbde94d78 | ||
|
|
11bbcf5b42 | ||
|
|
badf68b80a | ||
|
|
01e1c2ab0a | ||
|
|
bef51732e0 | ||
|
|
77046cf38a | ||
|
|
2a623957c3 | ||
|
|
65322925a4 | ||
|
|
346c4db3d9 | ||
|
|
7212698863 | ||
|
|
1779544d70 | ||
|
|
0270420da8 | ||
|
|
a1461851ee | ||
|
|
eaabad923e | ||
|
|
a9e12f5aca | ||
|
|
0637d5511d | ||
|
|
27682b2d07 | ||
|
|
0982852563 | ||
|
|
8147a315c2 | ||
|
|
fdf787e9b4 | ||
|
|
01d2338388 | ||
|
|
1dfec40f49 | ||
|
|
a82602faae | ||
|
|
c2aac371ae | ||
|
|
9aa6e0d39a | ||
|
|
3c177c4ebf | ||
|
|
d8b090bae2 | ||
|
|
928792bea7 | ||
|
|
99775194fd | ||
|
|
beb73828e0 | ||
|
|
87dc72b874 | ||
|
|
528ae68282 | ||
|
|
a44031cd39 | ||
|
|
af922d5171 | ||
|
|
bd6599a309 | ||
|
|
2e062b1ad6 | ||
|
|
5fc6654045 | ||
|
|
32c3494098 | ||
|
|
379461820b | ||
|
|
424cb7a9f2 | ||
|
|
b41e2b6f51 | ||
|
|
84e73f6f52 | ||
|
|
39fb42db10 | ||
|
|
85c18ce07b | ||
|
|
3fe548d8b7 | ||
|
|
eb32222d39 | ||
|
|
e70d7ab5c1 | ||
|
|
a3da735087 | ||
|
|
8c12b5a47e | ||
|
|
65c27c6c93 | ||
|
|
4c31e1a4b5 | ||
|
|
7be776d417 | ||
|
|
1a93e8f331 | ||
|
|
b6107f79dd | ||
|
|
fb0acb6a8b | ||
|
|
240a898fe5 | ||
|
|
3aad5916f5 | ||
|
|
c2a7f19d38 | ||
|
|
0a5fe2e2af | ||
|
|
f6e5256ae4 | ||
|
|
46aefe6b8a | ||
|
|
af7ef111b4 | ||
|
|
c918743187 | ||
|
|
817a691248 | ||
|
|
04e33448e0 | ||
|
|
aa4624e344 | ||
|
|
1b7a819a2b | ||
|
|
096036f2a4 | ||
|
|
b488ff053a | ||
|
|
04f24faf32 | ||
|
|
c180343853 | ||
|
|
da0739ab21 | ||
|
|
aa1d2a1879 | ||
|
|
9d9ef0069a | ||
|
|
50790b94a3 | ||
|
|
59b7f9c775 | ||
|
|
0f83f23052 | ||
|
|
1da95433ae | ||
|
|
d0579b5f75 | ||
|
|
1d19f56de8 | ||
|
|
3688e13137 | ||
|
|
45d71d0c5f | ||
|
|
d8be84b617 | ||
|
|
f7c4fe8e91 | ||
|
|
9005598f90 | ||
|
|
5e508770ef | ||
|
|
8331f92775 | ||
|
|
4c3c7e3807 | ||
|
|
1e39e3e815 | ||
|
|
5f6519d4c6 | ||
|
|
e93c35bcae | ||
|
|
a9d522d1d9 | ||
|
|
44ac42a4b5 | ||
|
|
78a60c750a | ||
|
|
6012b06a38 | ||
|
|
3ff370a903 | ||
|
|
6147d64a74 | ||
|
|
91b839dd26 | ||
|
|
cdb6d10519 | ||
|
|
586abb5466 | ||
|
|
710ac14202 | ||
|
|
a8735d1b4c | ||
|
|
f9ede752f2 | ||
|
|
5bdb205168 | ||
|
|
b606b939a6 | ||
|
|
0e0b12dcc0 | ||
|
|
b9e7b1f29b | ||
|
|
f930f2aa80 | ||
|
|
8698f7e4ab | ||
|
|
40b62c09f7 | ||
|
|
8ddc3ae172 | ||
|
|
b3e2fb4046 | ||
|
|
42558f2bd4 | ||
|
|
1e23b0a17e | ||
|
|
5b6e0d5c10 | ||
|
|
a53de75873 | ||
|
|
b3dc4671d0 | ||
|
|
ef4dd57032 | ||
|
|
ad1e17f329 | ||
|
|
2ba53e2dab | ||
|
|
909b8e7127 | ||
|
|
8280401e10 | ||
|
|
6096fc1c92 | ||
|
|
0378d54acc | ||
|
|
d33b642b8e | ||
|
|
27d68bcf45 | ||
|
|
b0122a8e38 | ||
|
|
96cbb63c84 | ||
|
|
bd16899c56 | ||
|
|
b874a2b76d | ||
|
|
aaf16273a9 | ||
|
|
f83e03e775 | ||
|
|
5328e443db | ||
|
|
ed49943039 | ||
|
|
220fdcdc75 | ||
|
|
a1b2a5149d | ||
|
|
3090a991a6 | ||
|
|
e0cae27f2f | ||
|
|
21c7c74b31 | ||
|
|
441693ac36 | ||
|
|
77910d82fc | ||
|
|
05258a5b48 | ||
|
|
387abd1606 | ||
|
|
3e4efc22a6 | ||
|
|
2851e51af8 | ||
|
|
ad8ad35c03 | ||
|
|
20d27bb9b3 | ||
|
|
30e878cbf9 | ||
|
|
374f6fcc1b | ||
|
|
4ea7d68bff | ||
|
|
88491f61e2 | ||
|
|
abe710f3ab | ||
|
|
3e4e87db23 | ||
|
|
e7ca1a13a6 | ||
|
|
aca78410f6 | ||
|
|
989331cbb4 | ||
|
|
7a8267a772 | ||
|
|
37ca383360 | ||
|
|
90eb55fbf8 | ||
|
|
e96c503b11 | ||
|
|
99ce6cf29d | ||
|
|
e14952607d | ||
|
|
de3efa0fe7 | ||
|
|
9af123523a | ||
|
|
7bc5adbde2 | ||
|
|
5287978e2f | ||
|
|
d364cc0f51 | ||
|
|
e7c30da23c | ||
|
|
1e6467cf80 | ||
|
|
12452c9b2a | ||
|
|
92b985d24d | ||
|
|
a0f640c739 | ||
|
|
46ecbc9c42 | ||
|
|
96f97e41e1 | ||
|
|
5726f05531 | ||
|
|
b389209c05 | ||
|
|
a82b3fd88a | ||
|
|
04a125d20f | ||
|
|
fc2e93a57f | ||
|
|
cfe9c17b58 | ||
|
|
b24f31f98c | ||
|
|
5263edc3e2 | ||
|
|
99b80cccad | ||
|
|
9794c5a188 | ||
|
|
2bd19736f4 | ||
|
|
13376d0ced | ||
|
|
1aa8475295 | ||
|
|
4bffe22f68 | ||
|
|
094941ab8c | ||
|
|
1a1bcc8d5c | ||
|
|
e77eb8a563 | ||
|
|
c063ccfa02 | ||
|
|
b1ccb5c3de | ||
|
|
29b4704c4b | ||
|
|
931d7d55dd | ||
|
|
2df0403c94 | ||
|
|
9cd878bad1 | ||
|
|
4c816eabc1 | ||
|
|
d28cbf3863 | ||
|
|
6269719b70 | ||
|
|
8a347e71ed | ||
|
|
9c44662f2e | ||
|
|
884291a8a3 | ||
|
|
97428909a4 | ||
|
|
a0bb52ff5f | ||
|
|
1a766c3ccc | ||
|
|
19bea70dc0 | ||
|
|
322a7020e3 | ||
|
|
242444ecc9 | ||
|
|
4820ebba84 | ||
|
|
7658481db2 | ||
|
|
f41644bc97 | ||
|
|
2163eb5910 | ||
|
|
ffbaf1e54f | ||
|
|
76ac47bbc6 | ||
|
|
d499f1af65 | ||
|
|
a249eeab22 |
21862
.gitattributes
vendored
21862
.gitattributes
vendored
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,9 @@
|
|||||||
<classpath>
|
<classpath>
|
||||||
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
|
<classpathentry kind="src" output="target/classes" path="src/main/java"/>
|
||||||
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
|
<classpathentry kind="src" output="target/test-classes" path="src/test/java"/>
|
||||||
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.7"/>
|
|
||||||
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
|
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
|
||||||
<classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
|
<classpathentry kind="con" path="org.testng.TESTNG_CONTAINER"/>
|
||||||
|
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER"/>
|
||||||
|
<classpathentry combineaccessrules="false" kind="src" path="/forge-core"/>
|
||||||
<classpathentry kind="output" path="target/classes"/>
|
<classpathentry kind="output" path="target/classes"/>
|
||||||
</classpath>
|
</classpath>
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
eclipse.preferences.version=1
|
eclipse.preferences.version=1
|
||||||
encoding//src/main/java=ISO-8859-1
|
encoding//src/main/java=ISO-8859-1
|
||||||
encoding//src/test/java=ISO-8859-1
|
|
||||||
encoding/<project>=UTF-8
|
encoding/<project>=UTF-8
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>forge</artifactId>
|
<artifactId>forge</artifactId>
|
||||||
<groupId>forge</groupId>
|
<groupId>forge</groupId>
|
||||||
<version>1.6.2</version>
|
<version>1.6.14</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>forge-ai</artifactId>
|
<artifactId>forge-ai</artifactId>
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -35,14 +30,21 @@ import forge.game.card.*;
|
|||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.combat.GlobalAttackRestrictions;
|
import forge.game.combat.GlobalAttackRestrictions;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.keyword.KeywordInterface;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
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.TextUtil;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
|
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
|
||||||
/**
|
/**
|
||||||
@@ -59,9 +61,6 @@ public class AiAttackController {
|
|||||||
private final List<Card> attackers;
|
private final List<Card> attackers;
|
||||||
private final List<Card> blockers;
|
private final List<Card> blockers;
|
||||||
|
|
||||||
private final static Random random = new Random();
|
|
||||||
private final static int randomInt = random.nextInt();
|
|
||||||
|
|
||||||
private List<Card> oppList; // holds human player creatures
|
private List<Card> oppList; // holds human player creatures
|
||||||
private List<Card> myList; // holds computer creatures
|
private List<Card> myList; // holds computer creatures
|
||||||
|
|
||||||
@@ -104,7 +103,7 @@ public class AiAttackController {
|
|||||||
} // overloaded constructor to evaluate single specified attacker
|
} // overloaded constructor to evaluate single specified attacker
|
||||||
|
|
||||||
public static List<Card> getOpponentCreatures(final Player defender) {
|
public static List<Card> getOpponentCreatures(final Player defender) {
|
||||||
List<Card> defenders = Lists.newArrayList();
|
List<Card> defenders = new ArrayList<Card>();
|
||||||
defenders.addAll(defender.getCreaturesInPlay());
|
defenders.addAll(defender.getCreaturesInPlay());
|
||||||
Predicate<Card> canAnimate = new Predicate<Card>() {
|
Predicate<Card> canAnimate = new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -203,8 +202,11 @@ public class AiAttackController {
|
|||||||
if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) {
|
if (ComputerUtilCombat.poisonIfUnblocked(attacker, opp) > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (this.attackers.size() == 1 && attacker.hasKeyword("Exalted")
|
|
||||||
&& ComputerUtilCombat.predictDamageTo(opp, 1, attacker, true) > 0) {
|
// TODO check if that makes sense
|
||||||
|
int exalted = ai.countExaltedBonus();
|
||||||
|
if (this.attackers.size() == 1 && exalted > 0
|
||||||
|
&& ComputerUtilCombat.predictDamageTo(opp, exalted, attacker, true) > 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,6 +245,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) {
|
||||||
@@ -253,6 +268,40 @@ public class AiAttackController {
|
|||||||
if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) {
|
if (ai.getGame().getPhaseHandler().getNextTurn().equals(ai)) {
|
||||||
return attackers;
|
return attackers;
|
||||||
}
|
}
|
||||||
|
// no need to block (already holding mana to cast fog next turn)
|
||||||
|
if (!AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) {
|
||||||
|
// Don't send the card that'll do the fog effect to attack, it's unsafe!
|
||||||
|
|
||||||
|
List<Card> toRemove = Lists.newArrayList();
|
||||||
|
for(Card c : attackers) {
|
||||||
|
if (AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT)) {
|
||||||
|
toRemove.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
attackers.removeAll(toRemove);
|
||||||
|
|
||||||
|
return attackers;
|
||||||
|
}
|
||||||
|
|
||||||
|
// no need to block if an effect is in play which untaps all creatures (pseudo-Vigilance akin to
|
||||||
|
// Awakening or Prophet of Kruphix)
|
||||||
|
for (Card card : ai.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||||
|
boolean untapsEachTurn = card.hasSVar("UntapsEachTurn");
|
||||||
|
boolean untapsEachOtherTurn = card.hasSVar("UntapsEachOtherPlayerTurn");
|
||||||
|
|
||||||
|
if (untapsEachTurn || untapsEachOtherTurn) {
|
||||||
|
String affected = untapsEachTurn ? card.getSVar("UntapsEachTurn")
|
||||||
|
: card.getSVar("UntapsEachOtherPlayerTurn");
|
||||||
|
|
||||||
|
for (String aff : TextUtil.split(affected, ',')) {
|
||||||
|
if (aff.equals("Creature")
|
||||||
|
&& (untapsEachTurn || (untapsEachOtherTurn && ai.equals(card.getController())))) {
|
||||||
|
return attackers;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
List<Card> opponentsAttackers = new ArrayList<Card>(oppList);
|
List<Card> opponentsAttackers = new ArrayList<Card>(oppList);
|
||||||
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() {
|
opponentsAttackers = CardLists.filter(opponentsAttackers, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -271,7 +320,7 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Vigilance")) {
|
if (c.hasKeyword(Keyword.VIGILANCE)) {
|
||||||
vigilantes.add(c);
|
vigilantes.add(c);
|
||||||
notNeededAsBlockers.remove(c); // they will be re-added later
|
notNeededAsBlockers.remove(c); // they will be re-added later
|
||||||
if (canBlockAnAttacker(c, opponentsAttackers, false)) {
|
if (canBlockAnAttacker(c, opponentsAttackers, false)) {
|
||||||
@@ -313,7 +362,7 @@ public class AiAttackController {
|
|||||||
// In addition, if the computer guesses it needs no blockers, make sure
|
// In addition, if the computer guesses it needs no blockers, make sure
|
||||||
// that
|
// that
|
||||||
// it won't be surprised by Exalted
|
// it won't be surprised by Exalted
|
||||||
final int humanExaltedBonus = countExaltedBonus(opp);
|
final int humanExaltedBonus = opp.countExaltedBonus();
|
||||||
|
|
||||||
if (humanExaltedBonus > 0) {
|
if (humanExaltedBonus > 0) {
|
||||||
final boolean finestHour = opp.isCardInPlay("Finest Hour");
|
final boolean finestHour = opp.isCardInPlay("Finest Hour");
|
||||||
@@ -362,8 +411,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()) {
|
||||||
@@ -402,7 +455,7 @@ public class AiAttackController {
|
|||||||
CardCollectionView oppBattlefield = c.getController().getCardsIn(ZoneType.Battlefield);
|
CardCollectionView oppBattlefield = c.getController().getCardsIn(ZoneType.Battlefield);
|
||||||
|
|
||||||
if (c.getName().equals("Heart of Kiran")) {
|
if (c.getName().equals("Heart of Kiran")) {
|
||||||
if (!CardLists.filter(oppBattlefield, CardPredicates.Presets.PLANEWALKERS).isEmpty()) {
|
if (!CardLists.filter(oppBattlefield, CardPredicates.Presets.PLANESWALKERS).isEmpty()) {
|
||||||
// can be activated by removing a loyalty counter instead of tapping a creature
|
// can be activated by removing a loyalty counter instead of tapping a creature
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -424,16 +477,45 @@ 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 each combat.")) {
|
||||||
for (Card attacker : this.attackers) {
|
for (Card attacker : this.attackers) {
|
||||||
if (CombatUtil.canBlock(attacker, blocker)) {
|
if (CombatUtil.canBlock(attacker, blocker)) {
|
||||||
remainingAttackers.remove(attacker);
|
remainingAttackers.remove(attacker);
|
||||||
@@ -449,7 +531,7 @@ public class AiAttackController {
|
|||||||
if (remainingAttackers.isEmpty() || maxBlockersAfterCrew == 0) {
|
if (remainingAttackers.isEmpty() || maxBlockersAfterCrew == 0) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (blocker.hasKeyword("CARDNAME can block an additional creature.")) {
|
if (blocker.hasKeyword("CARDNAME can block an additional creature each combat.")) {
|
||||||
blockedAttackers.add(remainingAttackers.get(0));
|
blockedAttackers.add(remainingAttackers.get(0));
|
||||||
remainingAttackers.remove(0);
|
remainingAttackers.remove(0);
|
||||||
maxBlockersAfterCrew--;
|
maxBlockersAfterCrew--;
|
||||||
@@ -465,7 +547,7 @@ public class AiAttackController {
|
|||||||
|
|
||||||
int trampleDamage = 0;
|
int trampleDamage = 0;
|
||||||
for (Card attacker : blockedAttackers) {
|
for (Card attacker : blockedAttackers) {
|
||||||
if (attacker.hasKeyword("Trample")) {
|
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
int damage = ComputerUtilCombat.getAttack(attacker);
|
int damage = ComputerUtilCombat.getAttack(attacker);
|
||||||
for (Card blocker : this.blockers) {
|
for (Card blocker : this.blockers) {
|
||||||
if (CombatUtil.canBlock(attacker, blocker)) {
|
if (CombatUtil.canBlock(attacker, blocker)) {
|
||||||
@@ -478,13 +560,15 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + ComputerUtil.possibleNonCombatDamage(ai)
|
int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage;
|
||||||
+ trampleDamage >= opp.getLife()
|
int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp);
|
||||||
|
|
||||||
|
if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai) >= opp.getLife()
|
||||||
&& !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) {
|
&& !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp) >= 10 - opp.getPoisonCounters()) {
|
if (totalPoisonDamage >= 10 - opp.getPoisonCounters()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,11 +582,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;
|
||||||
@@ -532,22 +621,26 @@ public class AiAttackController {
|
|||||||
* @return a {@link forge.game.combat.Combat} object.
|
* @return a {@link forge.game.combat.Combat} object.
|
||||||
*/
|
*/
|
||||||
public final void declareAttackers(final Combat combat) {
|
public final void declareAttackers(final Combat combat) {
|
||||||
// if this method is called multiple times during a turn,
|
|
||||||
// it will always return the same value
|
|
||||||
// randomInt is used so that the computer doesn't always
|
|
||||||
// do the same thing on turn 3 if he had the same creatures in play
|
|
||||||
// I know this is a little confusing
|
|
||||||
|
|
||||||
random.setSeed(ai.getGame().getPhaseHandler().getTurn() + AiAttackController.randomInt);
|
|
||||||
|
|
||||||
if (this.attackers.isEmpty()) {
|
if (this.attackers.isEmpty()) {
|
||||||
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");
|
||||||
@@ -585,7 +678,8 @@ public class AiAttackController {
|
|||||||
&& isEffectiveAttacker(ai, attacker, combat)) {
|
&& isEffectiveAttacker(ai, attacker, combat)) {
|
||||||
mustAttack = true;
|
mustAttack = true;
|
||||||
} else {
|
} else {
|
||||||
for (String s : attacker.getKeywords()) {
|
for (KeywordInterface inst : attacker.getKeywords()) {
|
||||||
|
String s = inst.getOriginal();
|
||||||
if (s.equals("CARDNAME attacks each turn if able.")
|
if (s.equals("CARDNAME attacks each turn if able.")
|
||||||
|| s.startsWith("CARDNAME attacks specific player each combat if able")
|
|| s.startsWith("CARDNAME attacks specific player each combat if able")
|
||||||
|| s.equals("CARDNAME attacks each combat if able.")) {
|
|| s.equals("CARDNAME attacks each combat if able.")) {
|
||||||
@@ -594,7 +688,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++;
|
||||||
@@ -639,8 +733,9 @@ public class AiAttackController {
|
|||||||
|
|
||||||
// Exalted
|
// Exalted
|
||||||
if (combat.getAttackers().isEmpty()) {
|
if (combat.getAttackers().isEmpty()) {
|
||||||
boolean exalted = false;
|
boolean exalted = ai.countExaltedBonus() > 2;
|
||||||
int exaltedCount = 0;
|
|
||||||
|
if (!exalted) {
|
||||||
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
|
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
|
||||||
if (c.getName().equals("Rafiq of the Many") || c.getName().equals("Battlegrace Angel")) {
|
if (c.getName().equals("Rafiq of the Many") || c.getName().equals("Battlegrace Angel")) {
|
||||||
exalted = true;
|
exalted = true;
|
||||||
@@ -650,12 +745,6 @@ public class AiAttackController {
|
|||||||
exalted = true;
|
exalted = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Exalted")) {
|
|
||||||
exaltedCount++;
|
|
||||||
if (exaltedCount > 2) {
|
|
||||||
exalted = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (exalted) {
|
if (exalted) {
|
||||||
@@ -715,7 +804,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);
|
||||||
@@ -723,8 +825,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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -851,8 +958,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
|
||||||
@@ -931,24 +1048,6 @@ public class AiAttackController {
|
|||||||
}
|
}
|
||||||
} // getAttackers()
|
} // getAttackers()
|
||||||
|
|
||||||
/**
|
|
||||||
* <p>
|
|
||||||
* countExaltedBonus.
|
|
||||||
* </p>
|
|
||||||
*
|
|
||||||
* @param player
|
|
||||||
* a {@link forge.game.player.Player} object.
|
|
||||||
* @return a int.
|
|
||||||
*/
|
|
||||||
public final static int countExaltedBonus(final Player player) {
|
|
||||||
int bonus = 0;
|
|
||||||
for (Card c : player.getCardsIn(ZoneType.Battlefield)) {
|
|
||||||
bonus += c.getAmountOfKeyword("Exalted");
|
|
||||||
}
|
|
||||||
|
|
||||||
return bonus;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* getAttack.
|
* getAttack.
|
||||||
@@ -961,7 +1060,7 @@ public class AiAttackController {
|
|||||||
public final static int getAttack(final Card c) {
|
public final static int getAttack(final Card c) {
|
||||||
int n = c.getNetCombatDamage();
|
int n = c.getNetCombatDamage();
|
||||||
|
|
||||||
if (c.hasKeyword("Double Strike")) {
|
if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
n *= 2;
|
n *= 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -990,6 +1089,28 @@ public class AiAttackController {
|
|||||||
boolean canBeBlocked = false;
|
boolean canBeBlocked = false;
|
||||||
int numberOfPossibleBlockers = 0;
|
int numberOfPossibleBlockers = 0;
|
||||||
|
|
||||||
|
// Is it a creature that has a more valuable ability with a tap cost than what it can do by attacking?
|
||||||
|
if ((attacker.hasSVar("NonCombatPriority"))
|
||||||
|
&& (!attacker.hasKeyword(Keyword.VIGILANCE))) {
|
||||||
|
// For each level of priority, enemy has to have life as much as the creature's power
|
||||||
|
// so a priority of 4 means the creature will not attack unless it can defeat that player in 4 successful attacks.
|
||||||
|
// the lower the priroity, the less willing the AI is to use the creature for attacking.
|
||||||
|
// TODO Somehow subtract expected damage of other attacking creatures from enemy life total (how? other attackers not yet declared? Can the AI guesstimate which of their creatures will not get blocked?)
|
||||||
|
if (attacker.getCurrentPower() * Integer.parseInt(attacker.getSVar("NonCombatPriority")) < ai.getOpponentsSmallestLifeTotal()) {
|
||||||
|
// Check if the card actually has an ability the AI can and wants to play, if not, attacking is fine!
|
||||||
|
boolean wantability = false;
|
||||||
|
for (SpellAbility sa : attacker.getSpellAbilities()) {
|
||||||
|
// Do not attack if we can afford using the ability.
|
||||||
|
if (sa.isAbility()) {
|
||||||
|
if (ComputerUtilCost.canPayCost(sa, ai)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// TODO Eventually The Ai will need to learn to predict if they have any use for the ability before next untap or not.
|
||||||
|
// TODO abilities that tap enemy creatures should probably only be saved if the enemy has nonzero creatures? Haste can be a threat though...
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.isEffectiveAttacker(ai, attacker, combat)) {
|
if (!this.isEffectiveAttacker(ai, attacker, combat)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -998,9 +1119,14 @@ public class AiAttackController {
|
|||||||
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
|
// is there a gain in attacking even when the blocker is not killed (Lifelink, Wither,...)
|
||||||
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|
boolean hasCombatEffect = attacker.getSVar("HasCombatEffect").equals("TRUE")
|
||||||
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
|| "Blocked".equals(attacker.getSVar("HasAttackEffect"));
|
||||||
|
// total power of the defending creatures, used in predicting whether a gang block can kill the attacker
|
||||||
|
int defPower = CardLists.getTotalPower(defenders, true);
|
||||||
|
|
||||||
if (!hasCombatEffect) {
|
if (!hasCombatEffect) {
|
||||||
for (String keyword : attacker.getKeywords()) {
|
for (KeywordInterface inst : attacker.getKeywords()) {
|
||||||
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
|
String keyword = inst.getOriginal();
|
||||||
|
if (keyword.equals("Wither") || keyword.equals("Infect")
|
||||||
|
|| keyword.equals("Lifelink") || keyword.startsWith("Afflict")) {
|
||||||
hasCombatEffect = true;
|
hasCombatEffect = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1017,7 +1143,7 @@ public class AiAttackController {
|
|||||||
&& CombatUtil.canBlock(attacker, defender)) {
|
&& CombatUtil.canBlock(attacker, defender)) {
|
||||||
numberOfPossibleBlockers += 1;
|
numberOfPossibleBlockers += 1;
|
||||||
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
|
if (isWorthLessThanAllKillers && ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, false)
|
||||||
&& !(attacker.hasKeyword("Undying") && attacker.getCounters(CounterType.P1P1) == 0)) {
|
&& !(attacker.hasKeyword(Keyword.UNDYING) && attacker.getCounters(CounterType.P1P1) == 0)) {
|
||||||
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
|
canBeKilledByOne = true; // there is a single creature on the battlefield that can kill the creature
|
||||||
// see if the defending creature is of higher or lower
|
// see if the defending creature is of higher or lower
|
||||||
// value. We don't want to attack only to lose value
|
// value. We don't want to attack only to lose value
|
||||||
@@ -1033,20 +1159,35 @@ public class AiAttackController {
|
|||||||
if (defender.getSVar("HasCombatEffect").equals("TRUE") || defender.getSVar("HasBlockEffect").equals("TRUE")) {
|
if (defender.getSVar("HasCombatEffect").equals("TRUE") || defender.getSVar("HasBlockEffect").equals("TRUE")) {
|
||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
} else {
|
} else {
|
||||||
for (String keyword : defender.getKeywords()) {
|
if (defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT)
|
||||||
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) {
|
|| defender.hasKeyword(Keyword.LIFELINK)) {
|
||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
break;
|
|
||||||
// there is a creature that can survive an attack from this creature
|
// there is a creature that can survive an attack from this creature
|
||||||
// and combat will have negative effects
|
// and combat will have negative effects
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if maybe we are too reckless in adding this attacker
|
||||||
|
if (canKillAllDangerous) {
|
||||||
|
boolean avoidAttackingIntoBlock = ai.getController().isAI()
|
||||||
|
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK);
|
||||||
|
boolean attackerWillDie = defPower >= attacker.getNetToughness();
|
||||||
|
boolean uselessAttack = !hasCombatEffect && !hasAttackEffect;
|
||||||
|
boolean noContributionToAttack = this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0;
|
||||||
|
|
||||||
|
// We are attacking too recklessly if we can't kill a single blocker and:
|
||||||
|
// - our creature will die for sure (chump attack)
|
||||||
|
// - our attack will not do anything special (no attack/combat effect to proc)
|
||||||
|
// - we can't deal damage to our opponent with sheer number of attackers and/or our attacker's power is 0 or less
|
||||||
|
if (attackerWillDie || (avoidAttackingIntoBlock && (uselessAttack || noContributionToAttack))) {
|
||||||
|
canKillAllDangerous = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!attacker.hasKeyword("vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
if (!attacker.hasKeyword(Keyword.VIGILANCE) && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
|
||||||
canKillAllDangerous = false;
|
canKillAllDangerous = false;
|
||||||
canBeKilled = true;
|
canBeKilled = true;
|
||||||
canBeKilledByOne = true;
|
canBeKilledByOne = true;
|
||||||
@@ -1132,7 +1273,7 @@ public class AiAttackController {
|
|||||||
// creature would leave the battlefield
|
// creature would leave the battlefield
|
||||||
// no pain in exerting it
|
// no pain in exerting it
|
||||||
shouldExert = true;
|
shouldExert = true;
|
||||||
} else if (c.hasKeyword("Vigilance")) {
|
} else if (c.hasKeyword(Keyword.VIGILANCE)) {
|
||||||
// Free exert - why not?
|
// Free exert - why not?
|
||||||
shouldExert = true;
|
shouldExert = true;
|
||||||
}
|
}
|
||||||
@@ -1151,18 +1292,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 && MyRandom.getRandom().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,21 @@ 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.keyword.Keyword;
|
||||||
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 +84,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 +99,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,7 +181,7 @@ public class AiBlockController {
|
|||||||
|
|
||||||
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||||
|| 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.")
|
||||||
|| attacker.hasKeyword("Menace")) {
|
|| attacker.hasKeyword(Keyword.MENACE)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,11 +205,11 @@ public class AiBlockController {
|
|||||||
&& !ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
&& !ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
||||||
blocker = ComputerUtilCard.getWorstCreatureAI(safeBlockers);
|
blocker = ComputerUtilCard.getWorstCreatureAI(safeBlockers);
|
||||||
// check whether it's better to block a creature without trample to absorb more damage
|
// check whether it's better to block a creature without trample to absorb more damage
|
||||||
if (attacker.hasKeyword("Trample")) {
|
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
boolean doNotBlock = false;
|
boolean doNotBlock = false;
|
||||||
for (Card other : attackersLeft) {
|
for (Card other : attackersLeft) {
|
||||||
if (other.equals(attacker) || !CombatUtil.canBlock(other, blocker)
|
if (other.equals(attacker) || !CombatUtil.canBlock(other, blocker)
|
||||||
|| other.hasKeyword("Trample")
|
|| other.hasKeyword(Keyword.TRAMPLE)
|
||||||
|| ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
|
|| ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
|
||||||
|| ComputerUtilCombat.canDestroyBlocker(ai, blocker, other, combat, false)
|
|| ComputerUtilCombat.canDestroyBlocker(ai, blocker, other, combat, false)
|
||||||
|| other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
|| other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||||
@@ -233,10 +232,9 @@ public class AiBlockController {
|
|||||||
// 3.Blockers that can destroy the attacker and have an upside when dying
|
// 3.Blockers that can destroy the attacker and have an upside when dying
|
||||||
killingBlockers = getKillingBlockers(combat, attacker, blockers);
|
killingBlockers = getKillingBlockers(combat, attacker, blockers);
|
||||||
for (Card b : killingBlockers) {
|
for (Card b : killingBlockers) {
|
||||||
if ((b.hasKeyword("Undying") && b.getCounters(CounterType.P1P1) == 0)
|
if ((b.hasKeyword(Keyword.UNDYING) && b.getCounters(CounterType.P1P1) == 0) || b.hasSVar("SacMe")
|
||||||
|| b.hasSVar("SacMe")
|
|| (b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|
||||||
|| (b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1)
|
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|
||||||
|| (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0)
|
|
||||||
|| b.hasSVar("EndOfTurnLeavePlay")) {
|
|| b.hasSVar("EndOfTurnLeavePlay")) {
|
||||||
blocker = b;
|
blocker = b;
|
||||||
break;
|
break;
|
||||||
@@ -295,8 +293,7 @@ public class AiBlockController {
|
|||||||
|
|
||||||
// 6. Blockers that don't survive until the next turn anyway
|
// 6. Blockers that don't survive until the next turn anyway
|
||||||
for (final Card attacker : attackersLeft) {
|
for (final Card attacker : attackersLeft) {
|
||||||
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)
|
||||||
|| attacker.hasKeyword("Menace")
|
|
||||||
|| 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;
|
||||||
}
|
}
|
||||||
@@ -306,8 +303,8 @@ public class AiBlockController {
|
|||||||
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||||
|
|
||||||
for (Card b : blockers) {
|
for (Card b : blockers) {
|
||||||
if ((b.hasStartOfKeyword("Vanishing") && b.getCounters(CounterType.TIME) == 1)
|
if ((b.hasKeyword(Keyword.VANISHING) && b.getCounters(CounterType.TIME) == 1)
|
||||||
|| (b.hasStartOfKeyword("Fading") && b.getCounters(CounterType.FADE) == 0)
|
|| (b.hasKeyword(Keyword.FADING) && b.getCounters(CounterType.FADE) == 0)
|
||||||
|| b.hasSVar("EndOfTurnLeavePlay")) {
|
|| b.hasSVar("EndOfTurnLeavePlay")) {
|
||||||
blocker = b;
|
blocker = b;
|
||||||
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
|
if (!ComputerUtilCombat.canDestroyAttacker(ai, attacker, blocker, combat, false)) {
|
||||||
@@ -417,7 +414,8 @@ public class AiBlockController {
|
|||||||
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
|
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker);
|
final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
|
||||||
|
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker) || randomTrade;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (usableBlockers.size() < 2) {
|
if (usableBlockers.size() < 2) {
|
||||||
@@ -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(Keyword.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.
|
||||||
@@ -542,22 +589,35 @@ public class AiBlockController {
|
|||||||
for (final Card attacker : attackersLeft) {
|
for (final Card attacker : attackersLeft) {
|
||||||
|
|
||||||
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||||
|| attacker.hasKeyword("Menace")
|
|| attacker.hasKeyword(Keyword.MENACE)
|
||||||
|| 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);
|
||||||
|
boolean doTrade = false;
|
||||||
|
|
||||||
|
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||||
|
// Always trade when life in danger
|
||||||
|
doTrade = true;
|
||||||
|
} else {
|
||||||
|
// Randomly trade creatures with lower power and [hopefully] worse abilities, if enabled in profile
|
||||||
|
doTrade = wouldLikeToRandomlyTrade(attacker, blocker, combat);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (doTrade) {
|
||||||
combat.addBlocker(attacker, blocker);
|
combat.addBlocker(attacker, blocker);
|
||||||
currentAttackers.remove(attacker);
|
currentAttackers.remove(attacker);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
attackersLeft = (new ArrayList<>(currentAttackers));
|
attackersLeft = (new ArrayList<>(currentAttackers));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -584,7 +644,7 @@ public class AiBlockController {
|
|||||||
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|
||||||
|| 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.")
|
||||||
|| attacker.hasKeyword("Menace")
|
|| attacker.hasKeyword(Keyword.MENACE)
|
||||||
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
|| ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
|
||||||
attackers.remove(0);
|
attackers.remove(0);
|
||||||
makeChumpBlocks(combat, attackers);
|
makeChumpBlocks(combat, attackers);
|
||||||
@@ -596,7 +656,7 @@ public class AiBlockController {
|
|||||||
final Card blocker = ComputerUtilCard.getWorstCreatureAI(chumpBlockers);
|
final Card blocker = ComputerUtilCard.getWorstCreatureAI(chumpBlockers);
|
||||||
|
|
||||||
// check if it's better to block a creature with lower power and without trample
|
// check if it's better to block a creature with lower power and without trample
|
||||||
if (attacker.hasKeyword("Trample")) {
|
if (attacker.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
final int damageAbsorbed = blocker.getLethalDamage();
|
final int damageAbsorbed = blocker.getLethalDamage();
|
||||||
if (attacker.getNetCombatDamage() > damageAbsorbed) {
|
if (attacker.getNetCombatDamage() > damageAbsorbed) {
|
||||||
for (Card other : attackers) {
|
for (Card other : attackers) {
|
||||||
@@ -604,7 +664,7 @@ public class AiBlockController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (other.getNetCombatDamage() >= damageAbsorbed
|
if (other.getNetCombatDamage() >= damageAbsorbed
|
||||||
&& !other.hasKeyword("Trample")
|
&& !other.hasKeyword(Keyword.TRAMPLE)
|
||||||
&& !other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|
&& !other.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|
||||||
&& !ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
|
&& !ComputerUtilCombat.attackerHasThreateningAfflict(other, ai)
|
||||||
&& CombatUtil.canBlock(other, blocker, combat)) {
|
&& CombatUtil.canBlock(other, blocker, combat)) {
|
||||||
@@ -635,7 +695,7 @@ public class AiBlockController {
|
|||||||
for (final Card attacker : currentAttackers) {
|
for (final Card attacker : currentAttackers) {
|
||||||
|
|
||||||
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||||
&& !attacker.hasKeyword("Menace")
|
&& !attacker.hasKeyword(Keyword.MENACE)
|
||||||
&& !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;
|
||||||
}
|
}
|
||||||
@@ -668,7 +728,7 @@ public class AiBlockController {
|
|||||||
|
|
||||||
List<Card> chumpBlockers;
|
List<Card> chumpBlockers;
|
||||||
|
|
||||||
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, "Trample");
|
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
|
||||||
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
|
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
|
||||||
|
|
||||||
// TODO - should check here for a "rampage-like" trigger that replaced
|
// TODO - should check here for a "rampage-like" trigger that replaced
|
||||||
@@ -677,7 +737,7 @@ public class AiBlockController {
|
|||||||
|
|
||||||
for (final Card attacker : tramplingAttackers) {
|
for (final Card attacker : tramplingAttackers) {
|
||||||
|
|
||||||
if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword("Menace")) && !combat.isBlocked(attacker))
|
if (((attacker.hasStartOfKeyword("CantBeBlockedByAmount LT") || attacker.hasKeyword(Keyword.MENACE)) && !combat.isBlocked(attacker))
|
||||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")
|
||||||
|| 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;
|
||||||
@@ -728,15 +788,15 @@ public class AiBlockController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// don't try to kill what can't be killed
|
// don't try to kill what can't be killed
|
||||||
if (attacker.hasKeyword("indestructible") || ComputerUtil.canRegenerate(ai, attacker)) {
|
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(ai, attacker)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try to add blockers that could be destroyed, but are worth less than the attacker
|
// Try to add blockers that could be destroyed, but are worth less than the attacker
|
||||||
// Don't use blockers without First Strike or Double Strike if attacker has it
|
// Don't use blockers without First Strike or Double Strike if attacker has it
|
||||||
if (ComputerUtilCombat.dealsFirstStrikeDamage(attacker, false, combat)) {
|
if (ComputerUtilCombat.dealsFirstStrikeDamage(attacker, false, combat)) {
|
||||||
safeBlockers = CardLists.getKeyword(blockers, "First Strike");
|
safeBlockers = CardLists.getKeyword(blockers, Keyword.FIRST_STRIKE);
|
||||||
safeBlockers.addAll(CardLists.getKeyword(blockers, "Double Strike"));
|
safeBlockers.addAll(CardLists.getKeyword(blockers, Keyword.DOUBLE_STRIKE));
|
||||||
} else {
|
} else {
|
||||||
safeBlockers = new ArrayList<>(blockers);
|
safeBlockers = new ArrayList<>(blockers);
|
||||||
}
|
}
|
||||||
@@ -760,6 +820,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(Keyword.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 +983,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 +1015,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
|
||||||
@@ -866,22 +1024,26 @@ public class AiBlockController {
|
|||||||
lifeInDanger = false;
|
lifeInDanger = false;
|
||||||
}
|
}
|
||||||
// if life is still in danger
|
// if life is still in danger
|
||||||
// Reinforce blockers blocking attackers with trample if life is still
|
// Reinforce blockers blocking attackers with trample if life is
|
||||||
|
// still
|
||||||
// in danger
|
// in danger
|
||||||
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||||
reinforceBlockersAgainstTrample(combat);
|
reinforceBlockersAgainstTrample(combat);
|
||||||
} else {
|
} else {
|
||||||
lifeInDanger = false;
|
lifeInDanger = false;
|
||||||
}
|
}
|
||||||
// Support blockers not destroying the attacker with more blockers to
|
// Support blockers not destroying the attacker with more blockers
|
||||||
|
// to
|
||||||
// try to kill the attacker
|
// try to kill the attacker
|
||||||
if (!lifeInDanger) {
|
if (!lifeInDanger) {
|
||||||
reinforceBlockersToKill(combat);
|
reinforceBlockersToKill(combat);
|
||||||
}
|
}
|
||||||
|
|
||||||
// == 2. If the AI life would still be in danger make a safer approach ==
|
// == 2. If the AI life would still be in danger make a safer
|
||||||
|
// approach ==
|
||||||
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||||
clearBlockers(combat, possibleBlockers); // reset every block assignment
|
clearBlockers(combat, possibleBlockers); // reset every block
|
||||||
|
// assignment
|
||||||
makeTradeBlocks(combat); // choose necessary trade blocks
|
makeTradeBlocks(combat); // choose necessary trade blocks
|
||||||
// if life is in danger
|
// if life is in danger
|
||||||
makeGoodBlocks(combat);
|
makeGoodBlocks(combat);
|
||||||
@@ -902,9 +1064,11 @@ public class AiBlockController {
|
|||||||
reinforceBlockersToKill(combat);
|
reinforceBlockersToKill(combat);
|
||||||
}
|
}
|
||||||
|
|
||||||
// == 3. If the AI life would be in serious danger make an even safer approach ==
|
// == 3. If the AI life would be in serious danger make an even
|
||||||
|
// safer approach ==
|
||||||
if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
|
if (lifeInDanger && ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
|
||||||
clearBlockers(combat, possibleBlockers); // reset every block assignment
|
clearBlockers(combat, possibleBlockers); // reset every block
|
||||||
|
// assignment
|
||||||
makeChumpBlocks(combat); // choose chump blocks
|
makeChumpBlocks(combat); // choose chump blocks
|
||||||
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||||
makeTradeBlocks(combat); // choose necessary trade
|
makeTradeBlocks(combat); // choose necessary trade
|
||||||
@@ -919,7 +1083,8 @@ public class AiBlockController {
|
|||||||
reinforceBlockersAgainstTrample(combat);
|
reinforceBlockersAgainstTrample(combat);
|
||||||
}
|
}
|
||||||
makeGangBlocks(combat);
|
makeGangBlocks(combat);
|
||||||
// Support blockers not destroying the attacker with more blockers
|
// Support blockers not destroying the attacker with more
|
||||||
|
// blockers
|
||||||
// to try to kill the attacker
|
// to try to kill the attacker
|
||||||
reinforceBlockersToKill(combat);
|
reinforceBlockersToKill(combat);
|
||||||
}
|
}
|
||||||
@@ -927,6 +1092,7 @@ public class AiBlockController {
|
|||||||
|
|
||||||
// assign blockers that have to block
|
// assign blockers that have to block
|
||||||
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each turn if able.");
|
chumpBlockers = CardLists.getKeyword(blockersLeft, "CARDNAME blocks each turn if able.");
|
||||||
|
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
|
||||||
// if an attacker with lure attacks - all that can block
|
// if an attacker with lure attacks - all that can block
|
||||||
for (final Card blocker : blockersLeft) {
|
for (final Card blocker : blockersLeft) {
|
||||||
if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
|
if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
|
||||||
@@ -940,7 +1106,8 @@ public class AiBlockController {
|
|||||||
for (final Card blocker : blockers) {
|
for (final Card blocker : blockers) {
|
||||||
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
|
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
|
||||||
&& (CombatUtil.mustBlockAnAttacker(blocker, combat)
|
&& (CombatUtil.mustBlockAnAttacker(blocker, combat)
|
||||||
|| blocker.hasKeyword("CARDNAME blocks each turn if able."))) {
|
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|
||||||
|
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
|
||||||
combat.addBlocker(attacker, blocker);
|
combat.addBlocker(attacker, blocker);
|
||||||
if (blocker.getMustBlockCards() != null) {
|
if (blocker.getMustBlockCards() != null) {
|
||||||
int mustBlockAmt = blocker.getMustBlockCards().size();
|
int mustBlockAmt = blocker.getMustBlockCards().size();
|
||||||
@@ -956,6 +1123,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)) {
|
||||||
@@ -994,7 +1173,8 @@ public class AiBlockController {
|
|||||||
* Orders a blocker that put onto the battlefield blocking. Depends heavily
|
* Orders a blocker that put onto the battlefield blocking. Depends heavily
|
||||||
* on the implementation of orderBlockers().
|
* on the implementation of orderBlockers().
|
||||||
*/
|
*/
|
||||||
public static CardCollection orderBlocker(final Card attacker, final Card blocker, final CardCollection oldBlockers) {
|
public static CardCollection orderBlocker(final Card attacker, final Card blocker,
|
||||||
|
final CardCollection oldBlockers) {
|
||||||
// add blocker to existing ordering
|
// add blocker to existing ordering
|
||||||
// sort by evaluate, then insert it appropriately
|
// sort by evaluate, then insert it appropriately
|
||||||
// relies on current implementation of orderBlockers()
|
// relies on current implementation of orderBlockers()
|
||||||
@@ -1009,17 +1189,21 @@ public class AiBlockController {
|
|||||||
boolean newBlockerIsAdded = false;
|
boolean newBlockerIsAdded = false;
|
||||||
// The new blocker comes right after this one
|
// The new blocker comes right after this one
|
||||||
final Card newBlockerRightAfter = (newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1));
|
final Card newBlockerRightAfter = (newBlockerIndex == 0 ? null : allBlockers.get(newBlockerIndex - 1));
|
||||||
if (newBlockerRightAfter == null && damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
|
if (newBlockerRightAfter == null
|
||||||
|
&& damage >= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
|
||||||
result.add(blocker);
|
result.add(blocker);
|
||||||
newBlockerIsAdded = true;
|
newBlockerIsAdded = true;
|
||||||
}
|
}
|
||||||
// Don't bother to keep damage up-to-date after the new blocker is added, as we can't modify the order of the other cards anyway
|
// Don't bother to keep damage up-to-date after the new blocker is
|
||||||
|
// added, as we can't modify the order of the other cards anyway
|
||||||
for (final Card c : oldBlockers) {
|
for (final Card c : oldBlockers) {
|
||||||
final int lethal = ComputerUtilCombat.getEnoughDamageToKill(c, damage, attacker, true);
|
final int lethal = ComputerUtilCombat.getEnoughDamageToKill(c, damage, attacker, true);
|
||||||
damage -= lethal;
|
damage -= lethal;
|
||||||
result.add(c);
|
result.add(c);
|
||||||
if (!newBlockerIsAdded && c == newBlockerRightAfter && damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
|
if (!newBlockerIsAdded && c == newBlockerRightAfter
|
||||||
// If blocker is right after this card in priority and we have sufficient damage to kill it, add it here
|
&& damage <= ComputerUtilCombat.getEnoughDamageToKill(blocker, damage, attacker, true)) {
|
||||||
|
// If blocker is right after this card in priority and we have
|
||||||
|
// sufficient damage to kill it, add it here
|
||||||
result.add(blocker);
|
result.add(blocker);
|
||||||
newBlockerIsAdded = true;
|
newBlockerIsAdded = true;
|
||||||
}
|
}
|
||||||
@@ -1056,4 +1240,91 @@ public class AiBlockController {
|
|||||||
return first;
|
return first;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean wouldLikeToRandomlyTrade(Card attacker, Card blocker, Combat combat) {
|
||||||
|
// Determines if the AI would like to randomly trade its blocker for the attacker in given combat
|
||||||
|
boolean enableRandomTrades = false;
|
||||||
|
boolean randomTradeIfBehindOnBoard = false;
|
||||||
|
boolean randomTradeIfCreatInHand = false;
|
||||||
|
int chanceModForEmbalm = 0;
|
||||||
|
int chanceToTradeToSaveWalker = 0;
|
||||||
|
int chanceToTradeDownToSaveWalker = 0;
|
||||||
|
int minRandomTradeChance = 0;
|
||||||
|
int maxRandomTradeChance = 0;
|
||||||
|
int maxCreatDiff = 0;
|
||||||
|
int maxCreatDiffWithRepl = 0;
|
||||||
|
int aiCreatureCount = 0;
|
||||||
|
int oppCreatureCount = 0;
|
||||||
|
if (ai.getController().isAI()) {
|
||||||
|
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
|
||||||
|
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
|
||||||
|
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
|
||||||
|
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
|
||||||
|
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||||
|
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
|
||||||
|
chanceModForEmbalm = aic.getIntProperty(AiProps.CHANCE_DECREASE_TO_TRADE_VS_EMBALM);
|
||||||
|
maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE);
|
||||||
|
maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL);
|
||||||
|
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
|
||||||
|
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableRandomTrades) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
aiCreatureCount = ComputerUtil.countUsefulCreatures(ai);
|
||||||
|
|
||||||
|
if (!attackersLeft.isEmpty()) {
|
||||||
|
oppCreatureCount = ComputerUtil.countUsefulCreatures(attackersLeft.get(0).getController());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attacker.getOwner().equals(ai) && "6".equals(attacker.getSVar("SacMe"))) {
|
||||||
|
// Temporarily controlled object - don't trade with it
|
||||||
|
// TODO: find a more reliable way to figure out that control will be reestablished next turn
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numSteps = ai.getStartingLife() - 5; // e.g. 15 steps between 5 life and 20 life
|
||||||
|
float chanceStep = (maxRandomTradeChance - minRandomTradeChance) / numSteps;
|
||||||
|
int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep));
|
||||||
|
if (chance > maxRandomTradeChance) {
|
||||||
|
chance = maxRandomTradeChance;
|
||||||
|
}
|
||||||
|
|
||||||
|
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false);
|
||||||
|
int evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
|
||||||
|
boolean atkEmbalm = (attacker.hasStartOfKeyword("Embalm") || attacker.hasStartOfKeyword("Eternalize")) && !attacker.isToken();
|
||||||
|
boolean blkEmbalm = (blocker.hasStartOfKeyword("Embalm") || blocker.hasStartOfKeyword("Eternalize")) && !blocker.isToken();
|
||||||
|
|
||||||
|
if (atkEmbalm && !blkEmbalm) {
|
||||||
|
// The opponent will eventually get his creature back, while the AI won't
|
||||||
|
chance = Math.max(0, chance - chanceModForEmbalm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocker.isFaceDown() && blocker.getState(CardStateName.Original).getType().isCreature()) {
|
||||||
|
// if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it
|
||||||
|
// in relation to the original state, not to the Morph state
|
||||||
|
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true);
|
||||||
|
}
|
||||||
|
int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
|
||||||
|
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();
|
||||||
|
boolean creatureParityOrAllowedDiff = aiCreatureCount
|
||||||
|
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
|
||||||
|
boolean wantToTradeWithCreatInHand = randomTradeIfCreatInHand
|
||||||
|
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).isEmpty()
|
||||||
|
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
|
||||||
|
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
|
||||||
|
&& combat.getDefenderByAttacker(attacker) instanceof Card
|
||||||
|
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
|
||||||
|
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
|
||||||
|
|
||||||
|
if (((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
|
||||||
|
&& powerParityOrHigher
|
||||||
|
&& (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand)
|
||||||
|
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,17 +41,27 @@ 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> memHeldManaSourcesForEnemyCombat;
|
||||||
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;
|
||||||
|
private final Set<Card> memChosenFogEffect;
|
||||||
|
|
||||||
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.memHeldManaSourcesForEnemyCombat = 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<>();
|
||||||
|
this.memChosenFogEffect = new HashSet<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -61,10 +71,15 @@ 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,
|
||||||
|
HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK,
|
||||||
ATTACHED_THIS_TURN,
|
ATTACHED_THIS_TURN,
|
||||||
ANIMATED_THIS_TURN,
|
ANIMATED_THIS_TURN,
|
||||||
BOUNCED_THIS_TURN,
|
BOUNCED_THIS_TURN,
|
||||||
|
ACTIVATED_THIS_TURN,
|
||||||
|
CHOSEN_FOG_EFFECT,
|
||||||
//REVEALED_CARDS // stub, not linked to AI code yet
|
//REVEALED_CARDS // stub, not linked to AI code yet
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,14 +87,24 @@ 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 HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK:
|
||||||
|
return memHeldManaSourcesForEnemyCombat;
|
||||||
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 CHOSEN_FOG_EFFECT:
|
||||||
|
return memChosenFogEffect;
|
||||||
//case REVEALED_CARDS:
|
//case REVEALED_CARDS:
|
||||||
// return memRevealedCards;
|
// return memRevealedCards;
|
||||||
default:
|
default:
|
||||||
@@ -249,10 +274,15 @@ 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.HELD_MANA_SOURCES_FOR_ENEMY_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);
|
||||||
|
clearMemorySet(MemorySet.CHOSEN_FOG_EFFECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 +295,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,52 +24,32 @@ 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.ability.ExploreAi;
|
||||||
import forge.ai.simulation.SpellAbilityPicker;
|
import forge.ai.simulation.SpellAbilityPicker;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.card.mana.ManaCost;
|
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.keyword.Keyword;
|
||||||
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 +60,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.
|
||||||
@@ -151,10 +127,12 @@ public class AiController {
|
|||||||
|
|
||||||
private List<SpellAbility> getPossibleETBCounters() {
|
private List<SpellAbility> getPossibleETBCounters() {
|
||||||
CardCollection all = new CardCollection(player.getCardsIn(ZoneType.Hand));
|
CardCollection all = new CardCollection(player.getCardsIn(ZoneType.Hand));
|
||||||
|
CardCollectionView ccvPlayerLibrary = player.getCardsIn(ZoneType.Library);
|
||||||
|
|
||||||
all.addAll(player.getCardsIn(ZoneType.Exile));
|
all.addAll(player.getCardsIn(ZoneType.Exile));
|
||||||
all.addAll(player.getCardsIn(ZoneType.Graveyard));
|
all.addAll(player.getCardsIn(ZoneType.Graveyard));
|
||||||
if (!player.getCardsIn(ZoneType.Library).isEmpty()) {
|
if (!ccvPlayerLibrary.isEmpty()) {
|
||||||
all.add(player.getCardsIn(ZoneType.Library).get(0));
|
all.add(ccvPlayerLibrary.get(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final Player opp : player.getOpponents()) {
|
for (final Player opp : player.getOpponents()) {
|
||||||
@@ -177,45 +155,50 @@ public class AiController {
|
|||||||
|
|
||||||
// look for cards on the battlefield that should prevent the AI from using that spellability
|
// look for cards on the battlefield that should prevent the AI from using that spellability
|
||||||
private boolean checkCurseEffects(final SpellAbility sa) {
|
private boolean checkCurseEffects(final SpellAbility sa) {
|
||||||
for (final Card c : game.getCardsIn(ZoneType.Battlefield)) {
|
CardCollectionView ccvGameBattlefield = game.getCardsIn(ZoneType.Battlefield);
|
||||||
|
for (final Card c : ccvGameBattlefield) {
|
||||||
if (c.hasSVar("AICurseEffect")) {
|
if (c.hasSVar("AICurseEffect")) {
|
||||||
final String curse = c.getSVar("AICurseEffect");
|
final String curse = c.getSVar("AICurseEffect");
|
||||||
final Card host = sa.getHostCard();
|
|
||||||
if ("NonActive".equals(curse) && !player.equals(game.getPhaseHandler().getPlayerTurn())) {
|
if ("NonActive".equals(curse) && !player.equals(game.getPhaseHandler().getPlayerTurn())) {
|
||||||
return true;
|
return true;
|
||||||
} else if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
|
} else {
|
||||||
&& !sa.getHostCard().hasKeyword("Indestructible")) {
|
final Card host = sa.getHostCard();
|
||||||
|
if ("DestroyCreature".equals(curse) && sa.isSpell() && host.isCreature()
|
||||||
|
&& !host.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
return true;
|
return true;
|
||||||
} else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
|
} else if ("CounterEnchantment".equals(curse) && sa.isSpell() && host.isEnchantment()
|
||||||
&& !sa.getHostCard().hasKeyword("CARDNAME can't be countered.")) {
|
&& CardFactoryUtil.isCounterable(host)) {
|
||||||
return true;
|
return true;
|
||||||
} else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.")
|
} else if ("ChaliceOfTheVoid".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)
|
||||||
&& host.getCMC() == c.getCounters(CounterType.CHARGE)) {
|
&& host.getCMC() == c.getCounters(CounterType.CHARGE)) {
|
||||||
return true;
|
return true;
|
||||||
} else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && !host.hasKeyword("CARDNAME can't be countered.")) {
|
} else if ("BazaarOfWonders".equals(curse) && sa.isSpell() && CardFactoryUtil.isCounterable(host)) {
|
||||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
String hostName = host.getName();
|
||||||
if (!card.isToken() && card.getName().equals(host.getName())) {
|
for (Card card : ccvGameBattlefield) {
|
||||||
|
if (!card.isToken() && card.getName().equals(hostName)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Card card : game.getCardsIn(ZoneType.Graveyard)) {
|
for (Card card : game.getCardsIn(ZoneType.Graveyard)) {
|
||||||
if (card.getName().equals(host.getName())) {
|
if (card.getName().equals(hostName)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean checkETBEffects(final Card card, final SpellAbility sa, final ApiType api) {
|
public boolean checkETBEffects(final Card card, final SpellAbility sa, final ApiType api) {
|
||||||
boolean rightapi = false;
|
|
||||||
|
|
||||||
if (card.isCreature()
|
if (card.isCreature()
|
||||||
&& game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureETBTriggers)) {
|
&& game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noCreatureETBTriggers)) {
|
||||||
return api == null;
|
return api == null;
|
||||||
}
|
}
|
||||||
|
boolean rightapi = false;
|
||||||
|
String battlefield = ZoneType.Battlefield.toString();
|
||||||
|
Player activatingPlayer = sa.getActivatingPlayer();
|
||||||
|
|
||||||
// Trigger play improvements
|
// Trigger play improvements
|
||||||
for (final Trigger tr : card.getTriggers()) {
|
for (final Trigger tr : card.getTriggers()) {
|
||||||
@@ -226,21 +209,22 @@ public class AiController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
|
if (!params.get("Destination").equals(battlefield)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.containsKey("ValidCard")) {
|
if (params.containsKey("ValidCard")) {
|
||||||
if (!params.get("ValidCard").contains("Self")) {
|
String validCard = params.get("ValidCard");
|
||||||
|
if (!validCard.contains("Self")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (params.get("ValidCard").contains("notkicked")) {
|
if (validCard.contains("notkicked")) {
|
||||||
if (sa.isKicked()) {
|
if (sa.isKicked()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (params.get("ValidCard").contains("kicked")) {
|
} else if (validCard.contains("kicked")) {
|
||||||
if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
|
if (validCard.contains("kicked ")) { // want a specific kicker
|
||||||
String s = params.get("ValidCard").split("kicked ")[1];
|
String s = validCard.split("kicked ")[1];
|
||||||
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
|
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
|
||||||
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
|
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
|
||||||
} else if (!sa.isKicked()) {
|
} else if (!sa.isKicked()) {
|
||||||
@@ -283,7 +267,7 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (sa != null) {
|
if (sa != null) {
|
||||||
exSA.setActivatingPlayer(sa.getActivatingPlayer());
|
exSA.setActivatingPlayer(activatingPlayer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
exSA.setActivatingPlayer(player);
|
exSA.setActivatingPlayer(player);
|
||||||
@@ -291,15 +275,13 @@ public class AiController {
|
|||||||
exSA.setTrigger(true);
|
exSA.setTrigger(true);
|
||||||
|
|
||||||
// for trigger test, need to ignore the conditions
|
// for trigger test, need to ignore the conditions
|
||||||
if (exSA.getConditions() != null) {
|
|
||||||
SpellAbilityCondition cons = exSA.getConditions();
|
SpellAbilityCondition cons = exSA.getConditions();
|
||||||
if (cons.getIsPresent() != null) {
|
if (cons != null) {
|
||||||
String pres = cons.getIsPresent();
|
String pres = cons.getIsPresent();
|
||||||
if ("Card.Self".equals(pres) || "Card.StrictlySelf".equals(pres)) {
|
if (pres != null && pres.matches("Card\\.(Strictly)?Self")) {
|
||||||
cons.setIsPresent(null);
|
cons.setIsPresent(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Run non-mandatory trigger.
|
// Run non-mandatory trigger.
|
||||||
// These checks only work if the Executing SpellAbility is an Ability_Sub.
|
// These checks only work if the Executing SpellAbility is an Ability_Sub.
|
||||||
@@ -321,21 +303,22 @@ public class AiController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!params.get("Destination").equals(ZoneType.Battlefield.toString())) {
|
if (!params.get("Destination").equals(battlefield)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (params.containsKey("ValidCard")) {
|
if (params.containsKey("ValidCard")) {
|
||||||
if (!params.get("ValidCard").contains("Self")) {
|
String validCard = params.get("ValidCard");
|
||||||
|
if (!validCard.contains("Self")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (params.get("ValidCard").contains("notkicked")) {
|
if (validCard.contains("notkicked")) {
|
||||||
if (sa.isKicked()) {
|
if (sa.isKicked()) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if (params.get("ValidCard").contains("kicked")) {
|
} else if (validCard.contains("kicked")) {
|
||||||
if (params.get("ValidCard").contains("kicked ")) { // want a specific kicker
|
if (validCard.contains("kicked ")) { // want a specific kicker
|
||||||
String s = params.get("ValidCard").split("kicked ")[1];
|
String s = validCard.split("kicked ")[1];
|
||||||
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
|
if ("1".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker1)) continue;
|
||||||
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
|
if ("2".equals(s) && !sa.isOptionalCostPaid(OptionalCost.Kicker2)) continue;
|
||||||
} else if (!sa.isKicked()) { // otherwise just any must be present
|
} else if (!sa.isKicked()) { // otherwise just any must be present
|
||||||
@@ -351,7 +334,7 @@ public class AiController {
|
|||||||
|
|
||||||
if (exSA != null) {
|
if (exSA != null) {
|
||||||
if (sa != null) {
|
if (sa != null) {
|
||||||
exSA.setActivatingPlayer(sa.getActivatingPlayer());
|
exSA.setActivatingPlayer(activatingPlayer);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
exSA.setActivatingPlayer(player);
|
exSA.setActivatingPlayer(player);
|
||||||
@@ -399,8 +382,9 @@ public class AiController {
|
|||||||
if (landsInPlay.size() + landList.size() > max) {
|
if (landsInPlay.size() + landList.size() > max) {
|
||||||
for (Card c : allCards) {
|
for (Card c : allCards) {
|
||||||
for (SpellAbility sa : c.getSpellAbilities()) {
|
for (SpellAbility sa : c.getSpellAbilities()) {
|
||||||
if (sa.getPayCosts() != null) {
|
Cost payCosts = sa.getPayCosts();
|
||||||
for (CostPart part : sa.getPayCosts().getCostParts()) {
|
if (payCosts != null) {
|
||||||
|
for (CostPart part : payCosts.getCostParts()) {
|
||||||
if (part instanceof CostDiscard) {
|
if (part instanceof CostDiscard) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -414,10 +398,11 @@ public class AiController {
|
|||||||
landList = CardLists.filter(landList, new Predicate<Card>() {
|
landList = CardLists.filter(landList, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
|
CardCollectionView battlefield = player.getCardsIn(ZoneType.Battlefield);
|
||||||
canPlaySpellBasic(c, null);
|
canPlaySpellBasic(c, null);
|
||||||
if (c.getType().isLegendary() && !c.getName().equals("Flagstones of Trokair")) {
|
String name = c.getName();
|
||||||
final CardCollectionView list = player.getCardsIn(ZoneType.Battlefield);
|
if (c.getType().isLegendary() && !name.equals("Flagstones of Trokair")) {
|
||||||
if (Iterables.any(list, CardPredicates.nameEquals(c.getName()))) {
|
if (Iterables.any(battlefield, CardPredicates.nameEquals(name))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -426,7 +411,7 @@ public class AiController {
|
|||||||
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
|
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
|
||||||
|
|
||||||
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
|
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
|
||||||
CardCollection lands = new CardCollection(player.getCardsIn(ZoneType.Battlefield));
|
CardCollection lands = new CardCollection(battlefield);
|
||||||
lands.addAll(hand);
|
lands.addAll(hand);
|
||||||
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
|
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
|
||||||
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
|
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
|
||||||
@@ -589,14 +574,17 @@ public class AiController {
|
|||||||
Collections.sort(all, saComparator); // put best spells first
|
Collections.sort(all, saComparator); // put best spells first
|
||||||
|
|
||||||
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
|
||||||
if (sa.getApi() == ApiType.Counter || sa.getApi() == exceptSA) {
|
ApiType saApi = sa.getApi();
|
||||||
|
|
||||||
|
if (saApi == ApiType.Counter || saApi == exceptSA) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player);
|
||||||
// TODO: this currently only works as a limited prediction of permanent spells.
|
// TODO: this currently only works as a limited prediction of permanent spells.
|
||||||
// Ideally this should cast canPlaySa to determine that the AI is truly able/willing to cast a spell,
|
// Ideally this should cast canPlaySa to determine that the AI is truly able/willing to cast a spell,
|
||||||
// but that is currently difficult to implement due to various side effects leading to stack overflow.
|
// but that is currently difficult to implement due to various side effects leading to stack overflow.
|
||||||
if (!ComputerUtil.castPermanentInMain1(player, sa) && sa.getHostCard() != null && !sa.getHostCard().isLand() && ComputerUtilCost.canPayCost(sa, player)) {
|
Card host = sa.getHostCard();
|
||||||
|
if (!ComputerUtil.castPermanentInMain1(player, sa) && host != null && !host.isLand() && ComputerUtilCost.canPayCost(sa, player)) {
|
||||||
if (sa instanceof SpellPermanent) {
|
if (sa instanceof SpellPermanent) {
|
||||||
return sa;
|
return sa;
|
||||||
}
|
}
|
||||||
@@ -605,11 +593,33 @@ public class AiController {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reserveManaSourcesForMain2(SpellAbility sa) {
|
public void reserveManaSources(SpellAbility sa) {
|
||||||
|
reserveManaSources(sa, PhaseType.MAIN2, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void reserveManaSources(SpellAbility sa, PhaseType phaseType, boolean enemy) {
|
||||||
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 = enemy ? AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK
|
||||||
|
: 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,6 +676,11 @@ public class AiController {
|
|||||||
if (sa instanceof SpellPermanent) {
|
if (sa instanceof SpellPermanent) {
|
||||||
return canPlayFromEffectAI((SpellPermanent)sa, false, true);
|
return canPlayFromEffectAI((SpellPermanent)sa, false, true);
|
||||||
}
|
}
|
||||||
|
if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
|
||||||
|
if (!sa.getTargetRestrictions().hasCandidates(sa, true)) {
|
||||||
|
return AiPlayDecision.TargetingFailed;
|
||||||
|
}
|
||||||
|
}
|
||||||
if (sa instanceof Spell) {
|
if (sa instanceof Spell) {
|
||||||
if (ComputerUtil.getDamageForPlaying(player, sa) >= player.getLife()
|
if (ComputerUtil.getDamageForPlaying(player, sa) >= player.getLife()
|
||||||
&& !player.cantLoseForZeroOrLessLife() && player.canLoseLife()) {
|
&& !player.cantLoseForZeroOrLessLife() && player.canLoseLife()) {
|
||||||
@@ -676,42 +691,34 @@ public class AiController {
|
|||||||
return AiPlayDecision.WillPlay;
|
return AiPlayDecision.WillPlay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isNonDisabledCardInPlay(final String cardName) {
|
||||||
|
for (Card card : player.getCardsIn(ZoneType.Battlefield)) {
|
||||||
|
if (card.getName().equals(cardName)) {
|
||||||
|
// TODO - Better logic to detemine if a permanent is disabled by local effects
|
||||||
|
// currently assuming any permanent enchanted by another player
|
||||||
|
// is disabled and a second copy is necessary
|
||||||
|
// will need actual logic that determines if the enchantment is able
|
||||||
|
// to disable the permanent or it's still functional and a duplicate is unneeded.
|
||||||
|
boolean disabledByEnemy = false;
|
||||||
|
for (Card card2 : card.getEnchantedBy(false)) {
|
||||||
|
if (card2.getOwner() != player) {
|
||||||
|
disabledByEnemy = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!disabledByEnemy) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
private AiPlayDecision canPlaySpellBasic(final Card card, final SpellAbility sa) {
|
private AiPlayDecision canPlaySpellBasic(final Card card, final SpellAbility sa) {
|
||||||
boolean isRightSplit = sa != null && sa.isRightSplit();
|
if ("True".equals(card.getSVar("NonStackingEffect")) && isNonDisabledCardInPlay(card.getName())) {
|
||||||
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
|
|
||||||
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar": "NeedsToPlayVar";
|
|
||||||
|
|
||||||
if (card.hasSVar(needsToPlayName)) {
|
|
||||||
final String needsToPlay = card.getSVar(needsToPlayName);
|
|
||||||
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
|
|
||||||
|
|
||||||
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
|
|
||||||
if (list.isEmpty()) {
|
|
||||||
return AiPlayDecision.MissingNeededCards;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (card.getSVar(needsToPlayVarName).length() > 0) {
|
|
||||||
final String needsToPlay = card.getSVar(needsToPlayVarName);
|
|
||||||
int x = 0;
|
|
||||||
int y = 0;
|
|
||||||
String sVar = needsToPlay.split(" ")[0];
|
|
||||||
String comparator = needsToPlay.split(" ")[1];
|
|
||||||
String compareTo = comparator.substring(2);
|
|
||||||
try {
|
|
||||||
x = Integer.parseInt(sVar);
|
|
||||||
} catch (final NumberFormatException e) {
|
|
||||||
x = CardFactoryUtil.xCount(card, card.getSVar(sVar));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
y = Integer.parseInt(compareTo);
|
|
||||||
} catch (final NumberFormatException e) {
|
|
||||||
y = CardFactoryUtil.xCount(card, card.getSVar(compareTo));
|
|
||||||
}
|
|
||||||
if (!Expressions.compare(x, comparator, y)) {
|
|
||||||
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||||
}
|
}
|
||||||
}
|
// add any other necessary logic to play a basic spell here
|
||||||
return AiPlayDecision.WillPlay;
|
return ComputerUtilCard.checkNeedsToPlayReqs(card, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
// not sure "playing biggest spell" matters?
|
// not sure "playing biggest spell" matters?
|
||||||
@@ -724,12 +731,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 +763,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,8 +778,14 @@ 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();
|
||||||
|
|
||||||
|
if (source != null) {
|
||||||
// puts creatures in front of spells
|
// puts creatures in front of spells
|
||||||
if (source.isCreature()) {
|
if (source.isCreature()) {
|
||||||
p += 1;
|
p += 1;
|
||||||
@@ -762,14 +794,9 @@ public class AiController {
|
|||||||
if (source.isEquipment() && noCreatures) {
|
if (source.isEquipment() && noCreatures) {
|
||||||
p -= 9;
|
p -= 9;
|
||||||
}
|
}
|
||||||
// use Surge and Prowl costs when able to
|
|
||||||
if (sa.isSurged() ||
|
|
||||||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
|
||||||
p += 9;
|
|
||||||
}
|
|
||||||
// 1. increase chance of using Surge effects
|
// 1. increase chance of using Surge effects
|
||||||
// 2. non-surged versions are usually inefficient
|
// 2. non-surged versions are usually inefficient
|
||||||
if (sa.getHostCard().getOracleText().contains("surge cost") && !sa.isSurged()) {
|
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
|
||||||
p -= 9;
|
p -= 9;
|
||||||
}
|
}
|
||||||
// move snap-casted spells to front
|
// move snap-casted spells to front
|
||||||
@@ -778,9 +805,16 @@ public class AiController {
|
|||||||
p += 50;
|
p += 50;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// artifacts and enchantments with effects that do not stack
|
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
|
||||||
if ("True".equals(source.getSVar("NonStackingEffect")) && ai.isCardInPlay(source.getName())) {
|
if (source.hasKeyword(Keyword.STORM) && ai.getController() instanceof PlayerControllerAi) {
|
||||||
p -= 9;
|
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// use Surge and Prowl costs when able to
|
||||||
|
if (sa.isSurged() ||
|
||||||
|
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
|
||||||
|
p += 9;
|
||||||
}
|
}
|
||||||
// sort planeswalker abilities with most costly first
|
// sort planeswalker abilities with most costly first
|
||||||
if (sa.getRestrictions().isPwAbility()) {
|
if (sa.getRestrictions().isPwAbility()) {
|
||||||
@@ -801,11 +835,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,7 +869,36 @@ 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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sa.hasParam("AnyNumber")) {
|
||||||
|
if ("DiscardUncastableAndExcess".equals(sa.getParam("AILogic"))) {
|
||||||
|
CardCollection discards = new CardCollection();
|
||||||
|
final CardCollectionView inHand = player.getCardsIn(ZoneType.Hand);
|
||||||
|
final int numLandsOTB = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||||
|
int numOppInHand = 0;
|
||||||
|
for (Player p : player.getGame().getPlayers()) {
|
||||||
|
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
|
||||||
|
numOppInHand = p.getCardsIn(ZoneType.Hand).size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Card c : inHand) {
|
||||||
|
if (c.hasSVar("DoNotDiscardIfAble") || c.hasSVar("IsReanimatorCard")) { continue; }
|
||||||
|
if (c.isCreature() && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getSpellPermanent(), player)) {
|
||||||
|
discards.add(c);
|
||||||
|
}
|
||||||
|
if ((c.isLand() && numLandsOTB >= 5) || (c.getFirstSpellAbility() != null && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getFirstSpellAbility(), player))) {
|
||||||
|
if (discards.size() + 1 <= numOppInHand) {
|
||||||
|
discards.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return discards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// look for good discards
|
// look for good discards
|
||||||
@@ -858,6 +916,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 +955,52 @@ 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);
|
||||||
|
}
|
||||||
|
if (worst == null && !validCards.isEmpty()) {
|
||||||
|
// still nothing chosen, so choose the first thing that works, trying not to make DoNotDiscardIfAble
|
||||||
|
// discards
|
||||||
|
for (Card c : validCards) {
|
||||||
|
if (!c.hasSVar("DoNotDiscardIfAble")) {
|
||||||
|
worst = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Only DoNotDiscardIfAble cards? If we have a duplicate for something, discard it
|
||||||
|
if (worst == null) {
|
||||||
|
for (Card c : validCards) {
|
||||||
|
if (CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c.getName())).size() > 1) {
|
||||||
|
worst = c;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (worst == null) {
|
||||||
|
// Otherwise just grab a random card and discard it
|
||||||
|
worst = Aggregates.random(validCards);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
discardList.add(worst);
|
discardList.add(worst);
|
||||||
validCards.remove(worst);
|
validCards.remove(worst);
|
||||||
}
|
}
|
||||||
@@ -1059,24 +1159,163 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
|
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
|
||||||
|
CardCollection playBeforeLand = CardLists.filter(
|
||||||
|
player.getCardsIn(ZoneType.Hand), CardPredicates.hasSVar("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);
|
||||||
if (landsWannaPlay != null && !landsWannaPlay.isEmpty() && player.canPlayLand(null)) {
|
if (landsWannaPlay != null && !landsWannaPlay.isEmpty() && player.canPlayLand(null)) {
|
||||||
|
// TODO search for other land it might want to play?
|
||||||
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();
|
final List<SpellAbility> abilities = Lists.newArrayList();
|
||||||
abilities.add(game.PLAY_LAND_SURROGATE);
|
|
||||||
|
LandAbility la = new LandAbility(land, player, null);
|
||||||
|
if (la.canPlay()) {
|
||||||
|
abilities.add(la);
|
||||||
|
}
|
||||||
|
|
||||||
|
// add mayPlay option
|
||||||
|
for (CardPlayOption o : land.mayPlay(player)) {
|
||||||
|
la = new LandAbility(land, player, o.getAbility());
|
||||||
|
if (la.canPlay()) {
|
||||||
|
abilities.add(la);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!abilities.isEmpty()) {
|
||||||
return abilities;
|
return abilities;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return singleSpellAbilityList(getSpellAbilityToPlay());
|
return singleSpellAbilityList(getSpellAbilityToPlay());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isSafeToHoldLandDropForMain2(Card landToPlay) {
|
||||||
|
boolean hasMomir = !CardLists.filter(player.getCardsIn(ZoneType.Command),
|
||||||
|
CardPredicates.nameEquals("Momir Vig, Simic Visionary Avatar")).isEmpty();
|
||||||
|
if (hasMomir) {
|
||||||
|
// Don't do this in Momir variants since it messes with the AI decision making for the avatar.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!MyRandom.percentTrue(getIntProperty(AiProps.HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED))) {
|
||||||
|
// check against the chance specified in the profile
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (game.getPhaseHandler().getTurn() <= 2) {
|
||||||
|
// too obvious when doing it on the very first turn of the game
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand),
|
||||||
|
Predicates.not(CardPredicates.Presets.LANDS));
|
||||||
|
CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
|
||||||
|
|
||||||
|
if (getBooleanProperty(AiProps.HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS)) {
|
||||||
|
if (CardLists.filter(otb, Predicates.not(CardPredicates.Presets.LANDS)).isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: improve the detection of taplands
|
||||||
|
boolean isTapLand = false;
|
||||||
|
for (ReplacementEffect repl : landToPlay.getReplacementEffects()) {
|
||||||
|
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
|
||||||
|
isTapLand = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int totalCMCInHand = Aggregates.sum(inHand, CardPredicates.Accessors.fnGetCmc);
|
||||||
|
int minCMCInHand = Aggregates.min(inHand, CardPredicates.Accessors.fnGetCmc);
|
||||||
|
int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
|
||||||
|
|
||||||
|
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && !isTapLand;
|
||||||
|
boolean cantCastAnythingNow = predictedMana < minCMCInHand;
|
||||||
|
|
||||||
|
boolean hasRelevantAbsOTB = !CardLists.filter(otb, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
boolean isTapLand = false;
|
||||||
|
for (ReplacementEffect repl : card.getReplacementEffects()) {
|
||||||
|
// TODO: improve the detection of taplands
|
||||||
|
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
|
||||||
|
isTapLand = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (SpellAbility sa : card.getSpellAbilities()) {
|
||||||
|
if (sa.getPayCosts() != null && sa.isAbility()
|
||||||
|
&& sa.getPayCosts().getCostMana() != null
|
||||||
|
&& sa.getPayCosts().getCostMana().getMana().getCMC() > 0
|
||||||
|
&& (!sa.getPayCosts().hasTapCost() || !isTapLand)
|
||||||
|
&& (!sa.hasParam("ActivationZone") || sa.getParam("ActivationZone").contains("Battlefield"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).isEmpty();
|
||||||
|
|
||||||
|
boolean hasLandBasedEffect = !CardLists.filter(otb, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
for (Trigger t : card.getTriggers()) {
|
||||||
|
Map<String, String> params = t.getMapParams();
|
||||||
|
if ("ChangesZone".equals(params.get("Mode"))
|
||||||
|
&& params.containsKey("ValidCard")
|
||||||
|
&& !params.get("ValidCard").contains("nonLand")
|
||||||
|
&& ((params.get("ValidCard").contains("Land")) || (params.get("ValidCard").contains("Permanent")))
|
||||||
|
&& "Battlefield".equals(params.get("Destination"))) {
|
||||||
|
// Landfall and other similar triggers
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (String sv : card.getSVars().keySet()) {
|
||||||
|
String varValue = card.getSVar(sv);
|
||||||
|
if (varValue.startsWith("Count$Valid") || sv.equals("BuffedBy")) {
|
||||||
|
if (varValue.contains("Land") || varValue.contains("Plains") || varValue.contains("Forest")
|
||||||
|
|| varValue.contains("Mountain") || varValue.contains("Island") || varValue.contains("Swamp")
|
||||||
|
|| varValue.contains("Wastes")) {
|
||||||
|
// In presence of various cards that get buffs like "equal to the number of lands you control",
|
||||||
|
// safer for our AI model to just play the land earlier rather than make a blunder
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}).isEmpty();
|
||||||
|
|
||||||
|
// TODO: add prediction for effects that will untap a tapland as it enters the battlefield
|
||||||
|
if (!canCastWithLandDrop && cantCastAnythingNow && !hasLandBasedEffect && (!hasRelevantAbsOTB || isTapLand)) {
|
||||||
|
// Hopefully there's not much to do with the extra mana immediately, can wait for Main 2
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((predictedMana <= totalCMCInHand && canCastWithLandDrop) || (hasRelevantAbsOTB && !isTapLand) || hasLandBasedEffect) {
|
||||||
|
// Might need an extra land to cast something, or for some kind of an ETB ability with a cost or an
|
||||||
|
// alternative cost (if we cast it in Main 1), or to use an activated ability on the battlefield
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private final SpellAbility getSpellAbilityToPlay() {
|
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)) {
|
||||||
@@ -1109,7 +1348,7 @@ public class AiController {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.getHostCard().hasKeyword("Storm")
|
if (sa.getHostCard().hasKeyword(Keyword.STORM)
|
||||||
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
|
&& sa.getApi() != ApiType.Counter // AI would suck at trying to deliberately proc a Storm counterspell
|
||||||
&& CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.hasKeyword("Storm")))).size() > 0) {
|
&& CardLists.filter(player.getCardsIn(ZoneType.Hand), Predicates.not(Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.hasKeyword("Storm")))).size() > 0) {
|
||||||
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
|
if (game.getView().getStormCount() < this.getIntProperty(AiProps.MIN_COUNT_FOR_STORM_SPELLS)) {
|
||||||
@@ -1337,11 +1576,11 @@ public class AiController {
|
|||||||
// and exaclty one counter of the specifice type gets high priority to keep the card
|
// and exaclty one counter of the specifice type gets high priority to keep the card
|
||||||
if (allies.contains(crd.getController())) {
|
if (allies.contains(crd.getController())) {
|
||||||
// except if its a Chronozoa, because it WANTS to be removed to make more
|
// except if its a Chronozoa, because it WANTS to be removed to make more
|
||||||
if (crd.hasKeyword("Vanishing") && !"Chronozoa".equals(crd.getName())) {
|
if (crd.hasKeyword(Keyword.VANISHING) && !"Chronozoa".equals(crd.getName())) {
|
||||||
if (crd.getCounters(CounterType.TIME) == 1) {
|
if (crd.getCounters(CounterType.TIME) == 1) {
|
||||||
return CounterType.TIME;
|
return CounterType.TIME;
|
||||||
}
|
}
|
||||||
} else if (crd.hasKeyword("Fading")) {
|
} else if (crd.hasKeyword(Keyword.FADING)) {
|
||||||
if (crd.getCounters(CounterType.FADE) == 1) {
|
if (crd.getCounters(CounterType.FADE) == 1) {
|
||||||
return CounterType.FADE;
|
return CounterType.FADE;
|
||||||
}
|
}
|
||||||
@@ -1571,8 +1810,12 @@ public class AiController {
|
|||||||
if (useSimulation) {
|
if (useSimulation) {
|
||||||
return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||||
}
|
}
|
||||||
|
if (sa.getApi() == ApiType.Explore) {
|
||||||
|
return ExploreAi.shouldPutInGraveyard(fetchList, decider);
|
||||||
|
} else {
|
||||||
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) {
|
public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) {
|
||||||
// list is only one or empty, no need to filter
|
// list is only one or empty, no need to filter
|
||||||
@@ -1593,6 +1836,10 @@ public class AiController {
|
|||||||
|
|
||||||
List<SpellAbility> evolve = filterList(putCounter, SpellAbilityPredicates.hasParam("Evolve"));
|
List<SpellAbility> evolve = filterList(putCounter, SpellAbilityPredicates.hasParam("Evolve"));
|
||||||
|
|
||||||
|
List<SpellAbility> token = filterListByApi(activePlayerSAs, ApiType.Token);
|
||||||
|
List<SpellAbility> pump = filterListByApi(activePlayerSAs, ApiType.Pump);
|
||||||
|
List<SpellAbility> pumpAll = filterListByApi(activePlayerSAs, ApiType.PumpAll);
|
||||||
|
|
||||||
// do mandatory discard early if hand is empty or has DiscardMe card
|
// do mandatory discard early if hand is empty or has DiscardMe card
|
||||||
boolean discardEarly = false;
|
boolean discardEarly = false;
|
||||||
CardCollectionView playerHand = player.getCardsIn(ZoneType.Hand);
|
CardCollectionView playerHand = player.getCardsIn(ZoneType.Hand);
|
||||||
@@ -1601,6 +1848,11 @@ public class AiController {
|
|||||||
result.addAll(mandatoryDiscard);
|
result.addAll(mandatoryDiscard);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// token should be added first so they might get the pump bonus
|
||||||
|
result.addAll(token);
|
||||||
|
result.addAll(pump);
|
||||||
|
result.addAll(pumpAll);
|
||||||
|
|
||||||
// do Evolve Trigger before other PutCounter SpellAbilities
|
// do Evolve Trigger before other PutCounter SpellAbilities
|
||||||
// do putCounter before Draw/Discard because it can cause a Draw Trigger
|
// do putCounter before Draw/Discard because it can cause a Draw Trigger
|
||||||
result.addAll(evolve);
|
result.addAll(evolve);
|
||||||
@@ -1616,6 +1868,9 @@ public class AiController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.addAll(activePlayerSAs);
|
result.addAll(activePlayerSAs);
|
||||||
|
|
||||||
|
//need to reverse because of magic stack
|
||||||
|
Collections.reverse(result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,8 +63,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
@Override
|
@Override
|
||||||
public PaymentDecision visit(CostDiscard cost) {
|
public PaymentDecision visit(CostDiscard cost) {
|
||||||
final String type = cost.getType();
|
final String type = cost.getType();
|
||||||
|
CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
|
||||||
|
|
||||||
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
|
|
||||||
if (type.equals("LastDrawn")) {
|
if (type.equals("LastDrawn")) {
|
||||||
if (!hand.contains(player.getLastDrawnCard())) {
|
if (!hand.contains(player.getLastDrawnCard())) {
|
||||||
return null;
|
return null;
|
||||||
@@ -79,6 +79,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
return PaymentDecision.card(source);
|
return PaymentDecision.card(source);
|
||||||
}
|
}
|
||||||
else if (type.equals("Hand")) {
|
else if (type.equals("Hand")) {
|
||||||
|
if (hand.size() > 1 && ability.getActivatingPlayer() != null) {
|
||||||
|
hand = ability.getActivatingPlayer().getController().orderMoveToZoneList(hand, ZoneType.Graveyard);
|
||||||
|
}
|
||||||
return PaymentDecision.card(hand);
|
return PaymentDecision.card(hand);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,7 +98,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (type.equals("Random")) {
|
if (type.equals("Random")) {
|
||||||
return PaymentDecision.card(CardLists.getRandomSubList(new CardCollection(hand), c));
|
CardCollectionView randomSubset = CardLists.getRandomSubList(new CardCollection(hand), c);
|
||||||
|
if (randomSubset.size() > 1 && ability.getActivatingPlayer() != null) {
|
||||||
|
randomSubset = ability.getActivatingPlayer().getController().orderMoveToZoneList(randomSubset, ZoneType.Graveyard);
|
||||||
|
}
|
||||||
|
return PaymentDecision.card(randomSubset);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
||||||
@@ -340,6 +347,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
if (source.getName().equals("Maralen of the Mornsong Avatar")) {
|
if (source.getName().equals("Maralen of the Mornsong Avatar")) {
|
||||||
return PaymentDecision.number(2);
|
return PaymentDecision.number(2);
|
||||||
}
|
}
|
||||||
|
if (source.getName().equals("Necrologia")) {
|
||||||
|
return PaymentDecision.number(Integer.parseInt(ability.getSVar("ChosenX")));
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
|
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
|
||||||
@@ -373,6 +383,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public PaymentDecision visit(CostPutCardToLib cost) {
|
public PaymentDecision visit(CostPutCardToLib cost) {
|
||||||
|
if (cost.payCostFromSource()) {
|
||||||
|
return PaymentDecision.card(source);
|
||||||
|
}
|
||||||
Integer c = cost.convertAmount();
|
Integer c = cost.convertAmount();
|
||||||
final Game game = player.getGame();
|
final Game game = player.getGame();
|
||||||
CardCollection chosen = new CardCollection();
|
CardCollection chosen = new CardCollection();
|
||||||
@@ -454,6 +467,10 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
ability.getActivatingPlayer(), ability.getHostCard(), ability);
|
ability.getActivatingPlayer(), ability.getHostCard(), ability);
|
||||||
typeList = CardLists.filter(typeList, Presets.UNTAPPED);
|
typeList = CardLists.filter(typeList, Presets.UNTAPPED);
|
||||||
c = typeList.size();
|
c = typeList.size();
|
||||||
|
// account for the fact that the activated card may be tapped in the process
|
||||||
|
if (ability.getPayCosts().hasTapCost() && typeList.contains(ability.getHostCard())) {
|
||||||
|
c--;
|
||||||
|
}
|
||||||
source.setSVar("ChosenX", "Number$" + Integer.toString(c));
|
source.setSVar("ChosenX", "Number$" + Integer.toString(c));
|
||||||
} else {
|
} else {
|
||||||
if (!isVehicle) {
|
if (!isVehicle) {
|
||||||
@@ -469,7 +486,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);
|
||||||
@@ -497,11 +514,27 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
|||||||
Integer c = cost.convertAmount();
|
Integer c = cost.convertAmount();
|
||||||
if (c == null) {
|
if (c == null) {
|
||||||
if (ability.getSVar(cost.getAmount()).equals("XChoice")) {
|
if (ability.getSVar(cost.getAmount()).equals("XChoice")) {
|
||||||
|
String logic = ability.getParamOrDefault("AILogic", "");
|
||||||
|
if ("SacToReduceCost".equals(logic)) {
|
||||||
|
// e.g. Torgaar, Famine Incarnate
|
||||||
|
// TODO: currently returns an empty list, so the AI doesn't sacrifice anything. Trying to make
|
||||||
|
// the AI decide on creatures to sac makes the AI sacrifice them, but the cost is not reduced and the
|
||||||
|
// AI pays the full mana cost anyway (despite sacrificing creatures).
|
||||||
|
return PaymentDecision.card(new CardCollection());
|
||||||
|
} else if (!logic.isEmpty() && !logic.equals("Never")) {
|
||||||
|
// If at least some other AI logic is specified, assume that the AI for that API knows how
|
||||||
|
// to define ChosenX and thus honor that value.
|
||||||
|
// Cards which have no special logic for this yet but which do work in a simple/suboptimal way
|
||||||
|
// are currently conventionally flagged with AILogic$ DoSacrifice.
|
||||||
|
c = AbilityUtils.calculateAmount(source, source.getSVar("ChosenX"), null);
|
||||||
|
} else {
|
||||||
|
// Other cards are assumed to be flagged RemAIDeck for now
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
|
c = AbilityUtils.calculateAmount(source, cost.getAmount(), ability);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
final AiController aic = ((PlayerControllerAi)player.getController()).getAi();
|
||||||
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);
|
CardCollectionView list = aic.chooseSacrificeType(cost.getType(), ability, c);
|
||||||
return PaymentDecision.card(list);
|
return PaymentDecision.card(list);
|
||||||
|
|||||||
@@ -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,51 @@ 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"), /** */
|
||||||
|
HOLD_LAND_DROP_ONLY_IF_HAVE_OTHER_PERMS ("true"), /** */
|
||||||
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_DECREASE_TO_TRADE_VS_EMBALM ("30"), /** */
|
||||||
|
CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER ("70"), /** */
|
||||||
|
CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER ("0"), /** */
|
||||||
|
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
|
||||||
|
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
|
||||||
|
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
|
||||||
|
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
|
||||||
|
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
|
||||||
|
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
|
||||||
|
CHANCE_TO_COUNTER_CMC_3 ("100"), /** */
|
||||||
ALWAYS_COUNTER_OTHER_COUNTERSPELLS ("true"), /** */
|
ALWAYS_COUNTER_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"), /** */
|
||||||
|
ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE ("false"), /** */
|
||||||
|
DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD ("2"), /** */
|
||||||
|
DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR ("true"), /** */
|
||||||
|
DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR ("5"), /** */
|
||||||
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
|
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 +81,37 @@ 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"), /** */
|
||||||
|
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
|
||||||
|
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE("2"), /** */
|
||||||
|
MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA("5"), /** */
|
||||||
|
MOJHOSTO_CHANCE_TO_PREFER_JHOIRA_OVER_MOMIR ("50"), /** */
|
||||||
|
MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT ("20"), /** */
|
||||||
|
// Experimental features, must be removed after extensive testing and, ideally, defaulting
|
||||||
|
// <-- There are no experimental options here -->
|
||||||
|
AI_IN_DANGER_THRESHOLD("4"),
|
||||||
|
AI_IN_DANGER_MAX_THRESHOLD("4");
|
||||||
|
|
||||||
|
|
||||||
private final String strDefaultVal;
|
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,18 @@ 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.keyword.Keyword;
|
||||||
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 +51,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.*;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,14 +75,28 @@ public class ComputerUtil {
|
|||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
if (sa.isSpell() && !source.isCopiedSpell()) {
|
if (sa.isSpell() && !source.isCopiedSpell()) {
|
||||||
|
if (source.getType().hasStringType("Arcane")) {
|
||||||
|
sa = AbilityUtils.addSpliceEffects(sa);
|
||||||
|
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
|
||||||
|
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
|
||||||
|
// otherwise the AI will fail to add the card to stack and that'll knock it out of the game
|
||||||
|
sa.resetTargets();
|
||||||
|
if (((PlayerControllerAi) ai.getController()).getAi().canPlaySa(sa) != AiPlayDecision.WillPlay) {
|
||||||
|
// for whatever reason the AI doesn't want to play the thing with the spliced subs anymore,
|
||||||
|
// proceeding past this point may result in an illegal play
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
source.setCastSA(sa);
|
source.setCastSA(sa);
|
||||||
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
||||||
sa.setLastStateGraveyard(game.getLastStateGraveyard());
|
sa.setLastStateGraveyard(game.getLastStateGraveyard());
|
||||||
sa.setHostCard(game.getAction().moveToStack(source, sa));
|
sa.setHostCard(game.getAction().moveToStack(source, sa));
|
||||||
|
|
||||||
if (source.getType().hasStringType("Arcane")) {
|
|
||||||
sa = AbilityUtils.addSpliceEffects(sa);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sa.isCopied()) {
|
||||||
|
sa.resetPaidHash();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
|
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
|
||||||
@@ -192,7 +182,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(ComputerUtil.getOpponentFor(ai), 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) {
|
||||||
@@ -272,6 +262,10 @@ public class ComputerUtil {
|
|||||||
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
sa.setLastStateBattlefield(game.getLastStateBattlefield());
|
||||||
sa.setLastStateGraveyard(game.getLastStateGraveyard());
|
sa.setLastStateGraveyard(game.getLastStateGraveyard());
|
||||||
newSA.setHostCard(game.getAction().moveToStack(source, sa));
|
newSA.setHostCard(game.getAction().moveToStack(source, sa));
|
||||||
|
|
||||||
|
if (newSA.getApi() == ApiType.Charm && !newSA.isWrapper()) {
|
||||||
|
CharmEffect.makeChoices(newSA);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
|
final CostPayment pay = new CostPayment(newSA.getPayCosts(), newSA);
|
||||||
@@ -310,18 +304,20 @@ 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);
|
|
||||||
CardCollection overrideList = null;
|
CardCollection overrideList = null;
|
||||||
|
|
||||||
if (activate.hasSVar("AIPreferenceOverride")) {
|
if (activate.hasSVar("AIPreferenceOverride")) {
|
||||||
overrideList = CardLists.getValidCards(typeList, activate.getSVar("AIPreferenceOverride"), activate.getController(), activate, null);
|
overrideList = CardLists.getValidCards(typeList, activate.getSVar("AIPreferenceOverride"), activate.getController(), activate, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (String validItem : prefValid[1].split(",")) {
|
||||||
|
final CardCollection prefList = CardLists.getValidCards(typeList, validItem, activate.getController(), activate, null);
|
||||||
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold");
|
int threshold = getAIPreferenceParameter(activate, "CreatureEvalThreshold");
|
||||||
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold");
|
int minNeeded = getAIPreferenceParameter(activate, "MinCreaturesBelowThreshold");
|
||||||
|
|
||||||
@@ -345,12 +341,13 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!prefList.isEmpty()) {
|
if (!prefList.isEmpty() || (overrideList != null && !overrideList.isEmpty())) {
|
||||||
return ComputerUtilCard.getWorstAI(overrideList == null ? prefList : overrideList);
|
return ComputerUtilCard.getWorstAI(overrideList == null ? prefList : overrideList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (pref.contains("SacCost")) {
|
if (pref.contains("SacCost")) {
|
||||||
// search for permanents with SacMe. priority 1 is the lowest, priority 5 the highest
|
// search for permanents with SacMe. priority 1 is the lowest, priority 5 the highest
|
||||||
for (int ip = 0; ip < 6; ip++) {
|
for (int ip = 0; ip < 6; ip++) {
|
||||||
@@ -411,6 +408,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()) {
|
||||||
@@ -707,6 +709,8 @@ public class ComputerUtil {
|
|||||||
return sacrificed; // sacrifice none
|
return sacrificed; // sacrifice none
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
boolean exceptSelf = "ExceptSelf".equals(source.getParam("AILogic"));
|
||||||
|
boolean removedSelf = false;
|
||||||
|
|
||||||
if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit") || considerSacLogic) {
|
if (isOptional && source.hasParam("Devour") || source.hasParam("Exploit") || considerSacLogic) {
|
||||||
if (source.hasParam("Exploit")) {
|
if (source.hasParam("Exploit")) {
|
||||||
@@ -764,11 +768,22 @@ public class ComputerUtil {
|
|||||||
|
|
||||||
final int max = Math.min(remaining.size(), amount);
|
final int max = Math.min(remaining.size(), amount);
|
||||||
|
|
||||||
|
if (exceptSelf) {
|
||||||
|
removedSelf = remaining.remove(source.getHostCard());
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = 0; i < max; i++) {
|
for (int i = 0; i < max; i++) {
|
||||||
Card c = chooseCardToSacrifice(remaining, ai, destroy);
|
Card c = chooseCardToSacrifice(remaining, ai, destroy);
|
||||||
remaining.remove(c);
|
remaining.remove(c);
|
||||||
|
if (c != null) {
|
||||||
sacrificed.add(c);
|
sacrificed.add(c);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sacrificed.isEmpty() && removedSelf) {
|
||||||
|
sacrificed.add(source.getHostCard());
|
||||||
|
}
|
||||||
|
|
||||||
return sacrificed;
|
return sacrificed;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,7 +796,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (destroy) {
|
if (destroy) {
|
||||||
final CardCollection indestructibles = CardLists.getKeyword(remaining, "Indestructible");
|
final CardCollection indestructibles = CardLists.getKeyword(remaining, Keyword.INDESTRUCTIBLE);
|
||||||
if (!indestructibles.isEmpty()) {
|
if (!indestructibles.isEmpty()) {
|
||||||
return indestructibles.get(0);
|
return indestructibles.get(0);
|
||||||
}
|
}
|
||||||
@@ -818,7 +833,7 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean canRegenerate(Player ai, final Card card) {
|
public static boolean canRegenerate(Player ai, final Card card) {
|
||||||
if (card.hasKeyword("CARDNAME can't be regenerated.")) {
|
if (!card.canBeShielded()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -848,11 +863,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 +883,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 +922,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -917,16 +932,12 @@ public class ComputerUtil {
|
|||||||
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
|
||||||
final Card card = sa.getHostCard();
|
final Card card = sa.getHostCard();
|
||||||
|
|
||||||
if ("True".equals(card.getSVar("NonStackingEffect")) && card.getController().isCardInPlay(card.getName())) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (card.hasSVar("PlayMain1")) {
|
if (card.hasSVar("PlayMain1")) {
|
||||||
if (card.getSVar("PlayMain1").equals("ALWAYS") || sa.getPayCosts().hasNoManaCost()) {
|
if (card.getSVar("PlayMain1").equals("ALWAYS") || sa.getPayCosts().hasNoManaCost()) {
|
||||||
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 (!ComputerUtil.getOpponentFor(card.getController()).getCreaturesInPlay().isEmpty()) {
|
if (!ai.getOpponents().getCreaturesInPlay().isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} else if (!card.getController().getCreaturesInPlay().isEmpty()) {
|
} else if (!card.getController().getCreaturesInPlay().isEmpty()) {
|
||||||
@@ -934,15 +945,25 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// try not to cast Raid creatures in main 1 if an attack is likely
|
||||||
|
if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword(Keyword.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;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.isCreature() && !card.hasKeyword("Defender") && (card.hasKeyword("Haste") || ComputerUtil.hasACardGivingHaste(ai) || sa.isDash())) {
|
if (card.isCreature() && !card.hasKeyword(Keyword.DEFENDER)
|
||||||
|
&& (card.hasKeyword(Keyword.HASTE) || ComputerUtil.hasACardGivingHaste(ai, true) || sa.isDash())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (card.hasKeyword("Exalted")) {
|
if (card.hasKeyword(Keyword.EXALTED)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -977,16 +998,16 @@ public class ComputerUtil {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (card.isCreature()) {
|
if (card.isCreature()) {
|
||||||
if (buffedcard.hasKeyword("Soulbond") && !buffedcard.isPaired()) {
|
if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (buffedcard.hasKeyword("Evolve")) {
|
if (buffedcard.hasKeyword(Keyword.EVOLVE)) {
|
||||||
if (buffedcard.getNetPower() < card.getNetPower() || buffedcard.getNetToughness() < card.getNetToughness()) {
|
if (buffedcard.getNetPower() < card.getNetPower() || buffedcard.getNetToughness() < card.getNetToughness()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (card.hasKeyword("Soulbond") && buffedcard.isCreature() && !buffedcard.isPaired()) {
|
if (card.hasKeyword(Keyword.SOULBOND) && buffedcard.isCreature() && !buffedcard.isPaired()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1111,6 +1132,10 @@ public class ComputerUtil {
|
|||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final SpellAbility sub = sa.getSubAbility();
|
final SpellAbility sub = sa.getSubAbility();
|
||||||
|
|
||||||
|
if (source != null && "ALWAYS".equals(source.getSVar("PlayMain1"))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Cipher spells
|
// Cipher spells
|
||||||
if (sub != null) {
|
if (sub != null) {
|
||||||
final ApiType api = sub.getApi();
|
final ApiType api = sub.getApi();
|
||||||
@@ -1168,16 +1193,14 @@ public class ComputerUtil {
|
|||||||
int activations = sa.getRestrictions().getNumberTurnActivations();
|
int activations = sa.getRestrictions().getNumberTurnActivations();
|
||||||
|
|
||||||
if (sa.isTemporary()) {
|
if (sa.isTemporary()) {
|
||||||
final Random r = MyRandom.getRandom();
|
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
|
||||||
return r.nextFloat() >= .95; // Abilities created by static abilities have no memory
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (activations < 10) { //10 activations per turn should still be acceptable
|
if (activations < 10) { //10 activations per turn should still be acceptable
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
return MyRandom.getRandom().nextFloat() >= Math.pow(.95, activations);
|
||||||
return r.nextFloat() >= Math.pow(.95, activations);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean activateForCost(SpellAbility sa, final Player ai) {
|
public static boolean activateForCost(SpellAbility sa, final Player ai) {
|
||||||
@@ -1220,8 +1243,8 @@ public class ComputerUtil {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasACardGivingHaste(final Player ai) {
|
public static boolean hasACardGivingHaste(final Player ai, final boolean checkOpponentCards) {
|
||||||
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
final CardCollection all = new CardCollection(ai.getCardsIn(Lists.newArrayList(ZoneType.Battlefield, ZoneType.Command)));
|
||||||
|
|
||||||
// Special for Anger
|
// Special for Anger
|
||||||
if (!ai.getGame().isCardInPlay("Yixlid Jailer")
|
if (!ai.getGame().isCardInPlay("Yixlid Jailer")
|
||||||
@@ -1232,7 +1255,7 @@ public class ComputerUtil {
|
|||||||
|
|
||||||
// Special for Odric
|
// Special for Odric
|
||||||
if (ai.isCardInPlay("Odric, Lunarch Marshal")
|
if (ai.isCardInPlay("Odric, Lunarch Marshal")
|
||||||
&& !CardLists.getKeyword(all, "Haste").isEmpty()) {
|
&& !CardLists.getKeyword(all, Keyword.HASTE).isEmpty()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1288,6 +1311,28 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (checkOpponentCards) {
|
||||||
|
// Check if the opponents have any cards giving Haste to all creatures on the battlefield
|
||||||
|
CardCollection opp = new CardCollection();
|
||||||
|
opp.addAll(ai.getOpponents().getCardsIn(ZoneType.Battlefield));
|
||||||
|
opp.addAll(ai.getOpponents().getCardsIn(ZoneType.Command));
|
||||||
|
|
||||||
|
for (final Card c : opp) {
|
||||||
|
for (StaticAbility stAb : c.getStaticAbilities()) {
|
||||||
|
Map<String, String> params = stAb.getMapParams();
|
||||||
|
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
|
||||||
|
&& params.get("AddKeyword").contains("Haste")) {
|
||||||
|
|
||||||
|
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(","));
|
||||||
|
if (affected.contains("Creature")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1315,7 +1360,7 @@ public class ComputerUtil {
|
|||||||
int damage = 0;
|
int damage = 0;
|
||||||
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||||
all.addAll(ai.getCardsActivableInExternalZones(true));
|
all.addAll(ai.getCardsActivableInExternalZones(true));
|
||||||
all.addAll(ai.getCardsIn(ZoneType.Hand));
|
all.addAll(CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(Presets.PERMANENTS)));
|
||||||
|
|
||||||
for (final Card c : all) {
|
for (final Card c : all) {
|
||||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||||
@@ -1340,7 +1385,26 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
damage = dmg;
|
damage = dmg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Triggered abilities
|
||||||
|
if (c.isCreature() && c.isInZone(ZoneType.Battlefield) && CombatUtil.canAttack(c)) {
|
||||||
|
for (final Trigger t : c.getTriggers()) {
|
||||||
|
if ("Attacks".equals(t.getParam("Mode")) && t.hasParam("Execute")) {
|
||||||
|
String exec = c.getSVar(t.getParam("Execute"));
|
||||||
|
if (!exec.isEmpty()) {
|
||||||
|
SpellAbility trigSa = AbilityFactory.getAbility(exec, c);
|
||||||
|
if (trigSa != null && trigSa.getApi() == ApiType.LoseLife
|
||||||
|
&& trigSa.getParamOrDefault("Defined", "").contains("Opponent")) {
|
||||||
|
trigSa.setHostCard(c);
|
||||||
|
damage += AbilityUtils.calculateAmount(trigSa.getHostCard(), trigSa.getParam("LifeAmount"), trigSa);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return damage;
|
return damage;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1436,17 +1500,26 @@ 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) {
|
||||||
|
toughness = saviorWithSubs.hasParam("NumDef") ?
|
||||||
|
AbilityUtils.calculateAmount(saviorWithSubs.getHostCard(), saviorWithSubs.getParam("NumDef"), saviour) : 0;
|
||||||
|
final List<String> keywords = saviorWithSubs.hasParam("KW") ?
|
||||||
|
Arrays.asList(saviorWithSubs.getParam("KW").split(" & ")) : new ArrayList<String>();
|
||||||
if (keywords.contains("Indestructible")) {
|
if (keywords.contains("Indestructible")) {
|
||||||
grantIndestructible = true;
|
grantIndestructible = true;
|
||||||
}
|
}
|
||||||
if (keywords.contains("Hexproof") || keywords.contains("Shroud")) {
|
if (keywords.contains("Hexproof") || keywords.contains("Shroud")) {
|
||||||
grantShroud = true;
|
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) {
|
||||||
@@ -1481,7 +1554,7 @@ public class ComputerUtil {
|
|||||||
final Card c = (Card) o;
|
final Card c = (Card) o;
|
||||||
|
|
||||||
// indestructible
|
// indestructible
|
||||||
if (c.hasKeyword("Indestructible")) {
|
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1528,7 +1601,7 @@ public class ComputerUtil {
|
|||||||
} else if (o instanceof Player) {
|
} else if (o instanceof Player) {
|
||||||
final Player p = (Player) o;
|
final Player p = (Player) o;
|
||||||
|
|
||||||
if (source.hasKeyword("Infect")) {
|
if (source.hasKeyword(Keyword.INFECT)) {
|
||||||
if (ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= p.getPoisonCounters()) {
|
if (ComputerUtilCombat.predictDamageTo(p, dmg, source, false) >= p.getPoisonCounters()) {
|
||||||
threatened.add(p);
|
threatened.add(p);
|
||||||
}
|
}
|
||||||
@@ -1549,14 +1622,14 @@ public class ComputerUtil {
|
|||||||
if (o instanceof Card) {
|
if (o instanceof Card) {
|
||||||
final Card c = (Card) o;
|
final Card c = (Card) o;
|
||||||
final boolean canRemove = (c.getNetToughness() <= dmg)
|
final boolean canRemove = (c.getNetToughness() <= dmg)
|
||||||
|| (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c)));
|
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && (dmg >= ComputerUtilCombat.getDamageToKill(c)));
|
||||||
if (!canRemove) {
|
if (!canRemove) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
|
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) {
|
||||||
final boolean cantSave = c.getNetToughness() + toughness <= dmg
|
final boolean cantSave = c.getNetToughness() + toughness <= dmg
|
||||||
|| (!c.hasKeyword("Indestructible") && c.getShieldCount() == 0 && !grantIndestructible
|
|| (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && c.getShieldCount() == 0 && !grantIndestructible
|
||||||
&& (dmg >= toughness + ComputerUtilCombat.getDamageToKill(c)));
|
&& (dmg >= toughness + ComputerUtilCombat.getDamageToKill(c)));
|
||||||
if (cantSave && (tgt == null || !grantShroud)) {
|
if (cantSave && (tgt == null || !grantShroud)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1590,12 +1663,13 @@ public class ComputerUtil {
|
|||||||
&& (((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;
|
||||||
// indestructible
|
// indestructible
|
||||||
if (c.hasKeyword("Indestructible")) {
|
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1604,7 +1678,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 +1933,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 +1968,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 +1978,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 +1996,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 +2012,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2530,7 +2636,7 @@ public class ComputerUtil {
|
|||||||
|| (type == CounterType.TIME && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|
|| (type == CounterType.TIME && (!c.isInPlay() || "Chronozoa".equals(c.getName())))
|
||||||
|| type == CounterType.GOLD || type == CounterType.MUSIC || type == CounterType.PUPA
|
|| type == CounterType.GOLD || type == CounterType.MUSIC || type == CounterType.PUPA
|
||||||
|| type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
|
|| type == CounterType.PARALYZATION || type == CounterType.SHELL || type == CounterType.SLEEP
|
||||||
|| type == CounterType.SLEIGHT || type == CounterType.WAGE;
|
|| type == CounterType.SLUMBER || type == CounterType.SLEIGHT || type == CounterType.WAGE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// this countertypes has no effect
|
// this countertypes has no effect
|
||||||
@@ -2685,21 +2791,122 @@ public class ComputerUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Deprecated
|
||||||
public static final Player getOpponentFor(final Player player) {
|
public static final Player getOpponentFor(final Player player) {
|
||||||
Player opponent = null;
|
// This method is deprecated and currently functions as a synonym for player.getWeakestOpponent
|
||||||
int minLife = Integer.MAX_VALUE;
|
// until it can be replaced everywhere in the code.
|
||||||
|
|
||||||
for (Player p : player.getOpponents()) {
|
|
||||||
if (p.getLife() < minLife) {
|
|
||||||
opponent = p;
|
|
||||||
minLife = p.getLife();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// 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) {
|
if (opponent != null) {
|
||||||
return opponent;
|
return opponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new IllegalStateException("No opponents left ingame for " + player);
|
throw new IllegalStateException("No opponents left ingame for " + player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static int countUsefulCreatures(Player p) {
|
||||||
|
CardCollection creats = p.getCreaturesInPlay();
|
||||||
|
int count = 0;
|
||||||
|
|
||||||
|
for (Card c : creats) {
|
||||||
|
if (!ComputerUtilCard.isUselessCreature(p, c)) {
|
||||||
|
count ++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean isPlayingReanimator(final Player ai) {
|
||||||
|
// TODO: either add SVars to other reanimator cards, or improve the prediction so that it avoids using a SVar
|
||||||
|
// at all but detects this effect from SA parameters (preferred, but difficult)
|
||||||
|
CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
|
||||||
|
CardCollectionView inDeck = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Library});
|
||||||
|
|
||||||
|
Predicate<Card> markedAsReanimator = new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return "true".equalsIgnoreCase(card.getSVar("IsReanimatorCard"));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int numInHand = CardLists.filter(inHand, markedAsReanimator).size();
|
||||||
|
int numInDeck = CardLists.filter(inDeck, markedAsReanimator).size();
|
||||||
|
|
||||||
|
return numInHand > 0 || numInDeck >= 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CardCollection filterAITgts(SpellAbility sa, Player ai, CardCollection srcList, boolean alwaysStrict) {
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
if (source == null) { return srcList; }
|
||||||
|
|
||||||
|
if (sa.hasParam("AITgts")) {
|
||||||
|
CardCollection list;
|
||||||
|
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
||||||
|
int value = ComputerUtilCard.evaluateCreature(source);
|
||||||
|
if (source.isEnchanted()) {
|
||||||
|
for (Card enc : source.getEnchantedBy(false)) {
|
||||||
|
if (enc.getController().equals(ai)) {
|
||||||
|
value += 100; // is 100 per AI's own aura enough?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final int totalValue = value;
|
||||||
|
list = CardLists.filter(srcList, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(final Card c) {
|
||||||
|
return ComputerUtilCard.evaluateCreature(c) > totalValue + 30;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
list = CardLists.getValidCards(srcList, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!list.isEmpty() || sa.hasParam("AITgtsStrict") || alwaysStrict) {
|
||||||
|
return list;
|
||||||
|
} else {
|
||||||
|
return srcList;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return srcList;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if AI life is in danger/serious danger based on next expected combat
|
||||||
|
// assuming a loss of "payment" life
|
||||||
|
// call this to determine if it's safe to use a life payment spell
|
||||||
|
// or trigger "emergency" strategies such as holding mana for Spike Weaver of Counterspell.
|
||||||
|
public static boolean aiLifeInDanger(Player ai, boolean serious, int payment) {
|
||||||
|
Player opponent = ComputerUtil.getOpponentFor(ai);
|
||||||
|
// test whether the human can kill the ai next turn
|
||||||
|
Combat combat = new Combat(opponent);
|
||||||
|
boolean containsAttacker = false;
|
||||||
|
for (Card att : opponent.getCreaturesInPlay()) {
|
||||||
|
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
|
||||||
|
combat.addAttacker(att, ai);
|
||||||
|
containsAttacker = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!containsAttacker) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
AiBlockController block = new AiBlockController(ai);
|
||||||
|
block.assignBlockersForCombat(combat);
|
||||||
|
|
||||||
|
// TODO predict other, noncombat sources of damage and add them to the "payment" variable.
|
||||||
|
// examples : Black Vise, The Rack, known direct damage spells in enemy hand, etc
|
||||||
|
// If added, might need a parameter to define whether we want to check all threats or combat threats.
|
||||||
|
|
||||||
|
if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,15 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Iterator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GameActionUtil;
|
import forge.game.GameActionUtil;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
@@ -17,7 +19,6 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import java.util.Iterator;
|
|
||||||
|
|
||||||
public class ComputerUtilAbility {
|
public class ComputerUtilAbility {
|
||||||
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
public static CardCollection getAvailableLandsToPlay(final Game game, final Player player) {
|
||||||
@@ -78,7 +79,7 @@ public class ComputerUtilAbility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<SpellAbility> getSpellAbilities(final CardCollectionView l, final Player player) {
|
public static List<SpellAbility> getSpellAbilities(final CardCollectionView l, final Player player) {
|
||||||
final List<SpellAbility> spellAbilities = new ArrayList<SpellAbility>();
|
final List<SpellAbility> spellAbilities = Lists.newArrayList();
|
||||||
for (final Card c : l) {
|
for (final Card c : l) {
|
||||||
for (final SpellAbility sa : c.getSpellAbilities()) {
|
for (final SpellAbility sa : c.getSpellAbilities()) {
|
||||||
spellAbilities.add(sa);
|
spellAbilities.add(sa);
|
||||||
@@ -93,7 +94,7 @@ public class ComputerUtilAbility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
|
public static List<SpellAbility> getOriginalAndAltCostAbilities(final List<SpellAbility> originList, final Player player) {
|
||||||
final List<SpellAbility> newAbilities = new ArrayList<SpellAbility>();
|
final List<SpellAbility> newAbilities = Lists.newArrayList();
|
||||||
for (SpellAbility sa : originList) {
|
for (SpellAbility sa : originList) {
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player);
|
||||||
//add alternative costs as additional spell abilities
|
//add alternative costs as additional spell abilities
|
||||||
@@ -101,7 +102,7 @@ public class ComputerUtilAbility {
|
|||||||
newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
|
newAbilities.addAll(GameActionUtil.getAlternativeCosts(sa, player));
|
||||||
}
|
}
|
||||||
|
|
||||||
final List<SpellAbility> result = new ArrayList<SpellAbility>();
|
final List<SpellAbility> result = Lists.newArrayList();
|
||||||
for (SpellAbility sa : newAbilities) {
|
for (SpellAbility sa : newAbilities) {
|
||||||
sa.setActivatingPlayer(player);
|
sa.setActivatingPlayer(player);
|
||||||
result.addAll(GameActionUtil.getOptionalCosts(sa));
|
result.addAll(GameActionUtil.getOptionalCosts(sa));
|
||||||
@@ -132,6 +133,42 @@ public class ComputerUtilAbility {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static String getAbilitySourceName(SpellAbility sa) {
|
public static String getAbilitySourceName(SpellAbility sa) {
|
||||||
return sa.getOriginalHost() != null ? sa.getOriginalHost().getName() : sa.getHostCard() != null ? sa.getHostCard().getName() : "";
|
final Card c = getAbilitySource(sa);
|
||||||
|
return c != null ? c.getName() : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
public static CardCollection getCardsTargetedWithApi(Player ai, CardCollection cardList, SpellAbility sa, ApiType api) {
|
||||||
|
// Returns a collection of cards which have already been targeted with the given API either in the parent ability,
|
||||||
|
// in the sub ability, or by something on stack. If "sa" is specified, the parent and sub abilities of this SA will
|
||||||
|
// be checked for targets. If "sa" is null, only the stack instances will be checked.
|
||||||
|
CardCollection targeted = new CardCollection();
|
||||||
|
if (sa != null) {
|
||||||
|
SpellAbility saSub = sa.getRootAbility();
|
||||||
|
while (saSub != null) {
|
||||||
|
if (saSub.getApi() == api && saSub.getTargets() != null) {
|
||||||
|
for (Card c : cardList) {
|
||||||
|
if (saSub.getTargets().getTargetCards().contains(c)) {
|
||||||
|
// Was already targeted with this API in a parent or sub SA
|
||||||
|
targeted.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saSub = saSub.getSubAbility();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (SpellAbilityStackInstance si : ai.getGame().getStack()) {
|
||||||
|
SpellAbility ab = si.getSpellAbility(false);
|
||||||
|
if (ab != null && ab.getApi() == api && si.getTargetChoices() != null) {
|
||||||
|
for (Card c : cardList) {
|
||||||
|
// TODO: somehow ensure that the detected SA won't be countered
|
||||||
|
if (si.getTargetChoices().getTargetCards().contains(c)) {
|
||||||
|
// Was already targeted by a spell ability instance on stack
|
||||||
|
targeted.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return targeted;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,9 @@
|
|||||||
package forge.ai;
|
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;
|
||||||
@@ -29,19 +18,13 @@ 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;
|
||||||
import forge.game.keyword.Keyword;
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.keyword.KeywordCollection;
|
||||||
|
import forge.game.keyword.KeywordInterface;
|
||||||
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;
|
||||||
@@ -54,8 +37,14 @@ import forge.game.zone.MagicStack;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.item.PaperCard;
|
import forge.item.PaperCard;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
|
import forge.util.Expressions;
|
||||||
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 {
|
||||||
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
|
public static Card getMostExpensivePermanentAI(final CardCollectionView list, final SpellAbility spell, final boolean targeted) {
|
||||||
@@ -107,14 +96,28 @@ public class ComputerUtilCard {
|
|||||||
* @return best Planeswalker
|
* @return best Planeswalker
|
||||||
*/
|
*/
|
||||||
public static Card getBestPlaneswalkerAI(final List<Card> list) {
|
public static Card getBestPlaneswalkerAI(final List<Card> list) {
|
||||||
List<Card> all = CardLists.filter(list, CardPredicates.Presets.PLANEWALKERS);
|
List<Card> all = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS);
|
||||||
if (all.size() == 0) {
|
if (all.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// no AI logic, just return most expensive
|
// no AI logic, just return most expensive
|
||||||
return Aggregates.itemWithMax(all, CardPredicates.Accessors.fnGetCmc);
|
return Aggregates.itemWithMax(all, CardPredicates.Accessors.fnGetCmc);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the worst Planeswalker from a given list
|
||||||
|
* @param list list of cards to evaluate
|
||||||
|
* @return best Planeswalker
|
||||||
|
*/
|
||||||
|
public static Card getWorstPlaneswalkerAI(final List<Card> list) {
|
||||||
|
List<Card> all = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS);
|
||||||
|
if (all.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
// no AI logic, just return least expensive
|
||||||
|
return Aggregates.itemWithMin(all, CardPredicates.Accessors.fnGetCmc);
|
||||||
|
}
|
||||||
|
|
||||||
// The AI doesn't really pick the best enchantment, just the most expensive.
|
// The AI doesn't really pick the best enchantment, just the most expensive.
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -364,7 +367,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 +385,32 @@ 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);
|
||||||
|
|
||||||
|
if (cc.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
Card cheapest = cc.getLast();
|
||||||
|
if (cheapest.hasSVar("DoNotDiscardIfAble")) {
|
||||||
|
for (int i = cc.size() - 1; i >= 0; i--) {
|
||||||
|
if (!cc.get(i).hasSVar("DoNotDiscardIfAble")) {
|
||||||
|
cheapest = cc.get(i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return cheapest;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
|
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 +433,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++) {
|
||||||
@@ -1043,7 +1081,7 @@ public class ComputerUtilCard {
|
|||||||
valueTempo *= 2; //deal with annoying things
|
valueTempo *= 2; //deal with annoying things
|
||||||
}
|
}
|
||||||
if (!destination.equals(ZoneType.Graveyard) && //TODO:boat-load of "when blah dies" triggers
|
if (!destination.equals(ZoneType.Graveyard) && //TODO:boat-load of "when blah dies" triggers
|
||||||
c.hasKeyword("Persist") || c.hasKeyword("Undying") || c.hasKeyword("Modular")) {
|
c.hasKeyword(Keyword.PERSIST) || c.hasKeyword(Keyword.UNDYING) || c.hasKeyword(Keyword.MODULAR)) {
|
||||||
valueTempo *= 2;
|
valueTempo *= 2;
|
||||||
}
|
}
|
||||||
if (destination.equals(ZoneType.Hand) && !c.isToken()) {
|
if (destination.equals(ZoneType.Hand) && !c.isToken()) {
|
||||||
@@ -1175,6 +1213,18 @@ 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"));
|
||||||
|
final boolean loseCardAtEOT = "Sacrifice".equals(sa.getParam("AtEOT")) || "Exile".equals(sa.getParam("AtEOT"))
|
||||||
|
|| "Destroy".equals(sa.getParam("AtEOT")) || "ExileCombat".equals(sa.getParam("AtEOT"));
|
||||||
|
|
||||||
|
boolean combatTrick = false;
|
||||||
|
boolean holdCombatTricks = false;
|
||||||
|
int chanceToHoldCombatTricks = -1;
|
||||||
|
|
||||||
|
if (ai.getController().isAI()) {
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||||
|
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
|
||||||
|
}
|
||||||
|
|
||||||
if (!c.canBeTargetedBy(sa)) {
|
if (!c.canBeTargetedBy(sa)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -1187,11 +1237,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
@@ -1205,8 +1255,8 @@ public class ComputerUtilCard {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// buff attacker/blocker using triggered pump
|
// buff attacker/blocker using triggered pump (unless it's lethal and we don't want to be reckless)
|
||||||
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !loseCardAtEOT) {
|
||||||
if (phase.isPlayerTurn(ai)) {
|
if (phase.isPlayerTurn(ai)) {
|
||||||
if (CombatUtil.canAttack(c)) {
|
if (CombatUtil.canAttack(c)) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1235,6 +1285,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
|
||||||
@@ -1262,7 +1334,7 @@ public class ComputerUtilCard {
|
|||||||
boolean pumpedWillDie = false;
|
boolean pumpedWillDie = false;
|
||||||
final boolean isAttacking = combat.isAttacking(c);
|
final boolean isAttacking = combat.isAttacking(c);
|
||||||
|
|
||||||
if (isBerserk && isAttacking) { pumpedWillDie = true; }
|
if ((isBerserk && isAttacking) || loseCardAtEOT) { pumpedWillDie = true; }
|
||||||
|
|
||||||
if (isAttacking) {
|
if (isAttacking) {
|
||||||
pumpedCombat.addAttacker(pumped, opp);
|
pumpedCombat.addAttacker(pumped, opp);
|
||||||
@@ -1286,8 +1358,9 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
//1. save combatant
|
//1. save combatant
|
||||||
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && !pumpedWillDie
|
if (ComputerUtilCombat.combatantWouldBeDestroyed(ai, c, combat) && !pumpedWillDie
|
||||||
&& !c.hasKeyword("Indestructible")) { // hack because attackerWouldBeDestroyed() does not
|
&& !c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
// check for Indestructible when computing lethal damage
|
// hack because attackerWouldBeDestroyed()
|
||||||
|
// does not check for Indestructible when computing lethal damage
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1320,11 +1393,21 @@ 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(Keyword.INFECT)) {
|
||||||
|
if (poisonPumped > poisonOrig) {
|
||||||
|
pumpedDmg = poisonPumped;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (combat.isBlocked(c)) {
|
if (combat.isBlocked(c)) {
|
||||||
if (!c.hasKeyword("Trample")) {
|
if (!c.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
dmg = 0;
|
dmg = 0;
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Trample") || keywords.contains("Trample")) {
|
if (c.hasKeyword(Keyword.TRAMPLE) || keywords.contains("Trample")) {
|
||||||
for (Card b : combat.getBlockers(c)) {
|
for (Card b : combat.getBlockers(c)) {
|
||||||
pumpedDmg -= ComputerUtilCombat.getDamageToKill(b);
|
pumpedDmg -= ComputerUtilCombat.getDamageToKill(b);
|
||||||
}
|
}
|
||||||
@@ -1332,15 +1415,18 @@ public class ComputerUtilCard {
|
|||||||
pumpedDmg = 0;
|
pumpedDmg = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (pumpedDmg >= opp.getLife()) {
|
if (pumpedDmg > dmg) {
|
||||||
|
if ((!c.hasKeyword(Keyword.INFECT) && pumpedDmg >= opp.getLife())
|
||||||
|
|| (c.hasKeyword(Keyword.INFECT) && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
|
||||||
return true;
|
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
|
||||||
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) {
|
if (phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && pumpedDmg > dmg) {
|
||||||
int totalPowerUnblocked = 0;
|
int totalPowerUnblocked = 0;
|
||||||
for (Card atk : combat.getAttackers()) {
|
for (Card atk : combat.getAttackers()) {
|
||||||
if (combat.isBlocked(atk) && !atk.hasKeyword("Trample")) {
|
if (combat.isBlocked(atk) && !atk.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (atk == c) {
|
if (atk == c) {
|
||||||
@@ -1373,7 +1459,7 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//4. lifelink
|
//4. lifelink
|
||||||
if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword("Lifelink") && keywords.contains("Lifelink")
|
if (ai.canGainLife() && ai.getLife() > 0 && !c.hasKeyword(Keyword.LIFELINK) && keywords.contains("Lifelink")
|
||||||
&& (combat.isAttacking(c) || combat.isBlocking(c))) {
|
&& (combat.isAttacking(c) || combat.isBlocking(c))) {
|
||||||
int dmg = pumped.getNetCombatDamage();
|
int dmg = pumped.getNetCombatDamage();
|
||||||
//The actual dmg inflicted should be the sum of ComputerUtilCombat.predictDamageTo() for opposing creature
|
//The actual dmg inflicted should be the sum of ComputerUtilCombat.predictDamageTo() for opposing creature
|
||||||
@@ -1386,7 +1472,7 @@ public class ComputerUtilCard {
|
|||||||
List<Card> blockedBy = combat.getAttackersBlockedBy(c);
|
List<Card> blockedBy = combat.getAttackersBlockedBy(c);
|
||||||
boolean attackerHasTrample = false;
|
boolean attackerHasTrample = false;
|
||||||
for (Card b : blockedBy) {
|
for (Card b : blockedBy) {
|
||||||
attackerHasTrample |= b.hasKeyword("Trample");
|
attackerHasTrample |= b.hasKeyword(Keyword.TRAMPLE);
|
||||||
}
|
}
|
||||||
if (attackerHasTrample && (sa.isAbility() || ComputerUtilCombat.lifeInDanger(ai, combat))) {
|
if (attackerHasTrample && (sa.isAbility() || ComputerUtilCombat.lifeInDanger(ai, combat))) {
|
||||||
return true;
|
return true;
|
||||||
@@ -1396,14 +1482,47 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
if (isBerserk) {
|
if (isBerserk) {
|
||||||
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
|
// if we got here, Berserk will result in the pumped creature dying at EOT and the opponent will not lose
|
||||||
|
// (other similar cards with AILogic$ Berserk that do not die only when attacking are excluded from consideration)
|
||||||
if (ai.getController() instanceof PlayerControllerAi) {
|
if (ai.getController() instanceof PlayerControllerAi) {
|
||||||
boolean aggr = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY);
|
boolean aggr = ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.USE_BERSERK_AGGRESSIVELY)
|
||||||
|
|| sa.hasParam("AtEOT");
|
||||||
if (!aggr) {
|
if (!aggr) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
boolean wantToHoldTrick = holdCombatTricks;
|
||||||
|
if (chanceToHoldCombatTricks >= 0) {
|
||||||
|
// Obey the chance specified in the AI profile for holding combat tricks
|
||||||
|
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
|
||||||
|
} else {
|
||||||
|
// Use standard considerations dependent solely on the buff chance determined above
|
||||||
|
wantToHoldTrick &= MyRandom.getRandom().nextFloat() < chance;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isHeldCombatTrick = combatTrick && wantToHoldTrick;
|
||||||
|
|
||||||
|
if (isHeldCombatTrick) {
|
||||||
|
if (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.TRICK_ATTACKERS)) {
|
||||||
|
// Attempt to hold combat tricks until blockers are declared, and try to lure the opponent into blocking
|
||||||
|
// (The AI will only do it for one attacker at the moment, otherwise it risks running his attackers into
|
||||||
|
// an army of opposing blockers with only one combat trick in hand)
|
||||||
|
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
|
||||||
|
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
|
||||||
|
// Reserve the mana until Declare Blockers such that the AI doesn't tap out before having a chance to use
|
||||||
|
// the combat trick
|
||||||
|
if (ai.getController().isAI()) {
|
||||||
|
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, false);
|
||||||
|
}
|
||||||
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1418,8 +1537,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();
|
||||||
@@ -1432,34 +1550,53 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Berserk (and other similar cards)
|
||||||
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
|
||||||
final int berserkPower = isBerserk ? c.getCurrentPower() : 0;
|
int berserkPower = 0;
|
||||||
|
if (isBerserk && sa.hasSVar("X")) {
|
||||||
|
if ("Targeted$CardPower".equals(sa.getSVar("X"))) {
|
||||||
|
berserkPower = c.getCurrentPower();
|
||||||
|
} else {
|
||||||
|
berserkPower = AbilityUtils.calculateAmount(sa.getHostCard(), "X", sa);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Electrostatic Pummeler
|
||||||
|
for (SpellAbility ab : c.getSpellAbilities()) {
|
||||||
|
if ("Pummeler".equals(ab.getParam("AILogic"))) {
|
||||||
|
Pair<Integer, Integer> newPT = SpecialCardAi.ElectrostaticPummeler.getPumpedPT(ai, power, toughness);
|
||||||
|
power = newPT.getLeft();
|
||||||
|
toughness = newPT.getRight();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
|
pumped.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);
|
||||||
pumped.addChangedCardKeywords(kws, new ArrayList<String>(), false, timestamp);
|
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
|
||||||
Set<CounterType> types = c.getCounters().keySet();
|
Set<CounterType> types = c.getCounters().keySet();
|
||||||
for(CounterType ct : types) {
|
for(CounterType ct : types) {
|
||||||
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), c, true);
|
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true);
|
||||||
}
|
}
|
||||||
//Copies tap-state and extra keywords (auras, equipment, etc.)
|
//Copies tap-state and extra keywords (auras, equipment, etc.)
|
||||||
if (c.isTapped()) {
|
if (c.isTapped()) {
|
||||||
pumped.setTapped(true);
|
pumped.setTapped(true);
|
||||||
}
|
}
|
||||||
final List<String> copiedKeywords = pumped.getKeywords();
|
|
||||||
List<String> toCopy = new ArrayList<String>();
|
KeywordCollection copiedKeywords = new KeywordCollection();
|
||||||
for (String kw : c.getKeywords()) {
|
copiedKeywords.insertAll(pumped.getKeywords());
|
||||||
if (!copiedKeywords.contains(kw)) {
|
List<KeywordInterface> toCopy = Lists.newArrayList();
|
||||||
if (kw.startsWith("HIDDEN")) {
|
for (KeywordInterface k : c.getKeywords()) {
|
||||||
pumped.addHiddenExtrinsicKeyword(kw);
|
if (!copiedKeywords.contains(k.getOriginal())) {
|
||||||
|
if (k.getHidden()) {
|
||||||
|
pumped.addHiddenExtrinsicKeyword(k);
|
||||||
} else {
|
} else {
|
||||||
toCopy.add(kw);
|
toCopy.add(k);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
|
final long timestamp2 = c.getGame().getNextTimestamp(); //is this necessary or can the timestamp be re-used?
|
||||||
pumped.addChangedCardKeywords(toCopy, new ArrayList<String>(), false, timestamp2);
|
pumped.addChangedCardKeywordsInternal(toCopy, null, false, false, timestamp2, true);
|
||||||
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
|
ComputerUtilCard.applyStaticContPT(ai.getGame(), pumped, new CardCollection(c));
|
||||||
return pumped;
|
return pumped;
|
||||||
}
|
}
|
||||||
@@ -1488,6 +1625,9 @@ public class ComputerUtilCard {
|
|||||||
if (!params.containsKey("Affected")) {
|
if (!params.containsKey("Affected")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
final String valid = params.get("Affected");
|
final String valid = params.get("Affected");
|
||||||
if (!vCard.isValid(valid, c.getController(), c, null)) {
|
if (!vCard.isValid(valid, c.getController(), c, null)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1577,10 +1717,10 @@ public class ComputerUtilCard {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static boolean hasActiveUndyingOrPersist(final Card c) {
|
public static boolean hasActiveUndyingOrPersist(final Card c) {
|
||||||
if (c.hasKeyword("Undying") && c.getCounters(CounterType.P1P1) == 0) {
|
if (c.hasKeyword(Keyword.UNDYING) && c.getCounters(CounterType.P1P1) == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Persist") && c.getCounters(CounterType.M1M1) == 0) {
|
if (c.hasKeyword(Keyword.PERSIST) && c.getCounters(CounterType.M1M1) == 0) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -1613,4 +1753,95 @@ public class ComputerUtilCard {
|
|||||||
|
|
||||||
return maxEnergyCost;
|
return maxEnergyCost;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static CardCollection prioritizeCreaturesWorthRemovingNow(final Player ai, CardCollection oppCards, final boolean temporary) {
|
||||||
|
if (!CardLists.getNotType(oppCards, "Creature").isEmpty()) {
|
||||||
|
// non-creatures were passed, nothing to do here
|
||||||
|
return oppCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean enablePriorityRemoval = false;
|
||||||
|
boolean priorityRemovalOnlyInDanger = false;
|
||||||
|
int priorityRemovalThreshold = 0;
|
||||||
|
int lifeInDanger = 5;
|
||||||
|
if (ai.getController().isAI()) {
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
enablePriorityRemoval = aic.getBooleanProperty(AiProps.ACTIVELY_DESTROY_IMMEDIATELY_UNBLOCKABLE);
|
||||||
|
priorityRemovalThreshold = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_THRESHOLD);
|
||||||
|
priorityRemovalOnlyInDanger = aic.getBooleanProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_ONLY_IN_DNGR);
|
||||||
|
lifeInDanger = aic.getIntProperty(AiProps.DESTROY_IMMEDIATELY_UNBLOCKABLE_LIFE_IN_DNGR);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enablePriorityRemoval) {
|
||||||
|
// Nothing to do here, the profile does not allow prioritizing
|
||||||
|
return oppCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardCollection aiCreats = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
|
||||||
|
if (temporary) {
|
||||||
|
// Pump effects that add "CARDNAME can't attack" and similar things. Only do it if something is untapped.
|
||||||
|
oppCards = CardLists.filter(oppCards, CardPredicates.Presets.UNTAPPED);
|
||||||
|
}
|
||||||
|
|
||||||
|
CardCollection priorityCards = new CardCollection();
|
||||||
|
for (Card atk : oppCards) {
|
||||||
|
if (isUselessCreature(atk.getController(), atk)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (Card blk : aiCreats) {
|
||||||
|
if (!CombatUtil.canBlock(atk, blk, true)) {
|
||||||
|
boolean threat = atk.getNetCombatDamage() >= ai.getLife() - lifeInDanger;
|
||||||
|
if (!priorityRemovalOnlyInDanger || threat) {
|
||||||
|
priorityCards.add(atk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!priorityCards.isEmpty() && priorityCards.size() <= priorityRemovalThreshold) {
|
||||||
|
return priorityCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
return oppCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static AiPlayDecision checkNeedsToPlayReqs(final Card card, final SpellAbility sa) {
|
||||||
|
Game game = card.getGame();
|
||||||
|
boolean isRightSplit = sa != null && sa.isRightSplit();
|
||||||
|
String needsToPlayName = isRightSplit ? "SplitNeedsToPlay" : "NeedsToPlay";
|
||||||
|
String needsToPlayVarName = isRightSplit ? "SplitNeedsToPlayVar" : "NeedsToPlayVar";
|
||||||
|
|
||||||
|
if (card.hasSVar(needsToPlayName)) {
|
||||||
|
final String needsToPlay = card.getSVar(needsToPlayName);
|
||||||
|
CardCollectionView list = game.getCardsIn(ZoneType.Battlefield);
|
||||||
|
|
||||||
|
list = CardLists.getValidCards(list, needsToPlay.split(","), card.getController(), card, null);
|
||||||
|
if (list.isEmpty()) {
|
||||||
|
return AiPlayDecision.MissingNeededCards;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (card.getSVar(needsToPlayVarName).length() > 0) {
|
||||||
|
final String needsToPlay = card.getSVar(needsToPlayVarName);
|
||||||
|
int x = 0;
|
||||||
|
int y = 0;
|
||||||
|
String sVar = needsToPlay.split(" ")[0];
|
||||||
|
String comparator = needsToPlay.split(" ")[1];
|
||||||
|
String compareTo = comparator.substring(2);
|
||||||
|
try {
|
||||||
|
x = Integer.parseInt(sVar);
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
x = CardFactoryUtil.xCount(card, card.getSVar(sVar));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
y = Integer.parseInt(compareTo);
|
||||||
|
} catch (final NumberFormatException e) {
|
||||||
|
y = CardFactoryUtil.xCount(card, card.getSVar(compareTo));
|
||||||
|
}
|
||||||
|
if (!Expressions.compare(x, comparator, y)) {
|
||||||
|
return AiPlayDecision.NeedsToPlayCriteriaNotMet;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return AiPlayDecision.WillPlay;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,16 +32,12 @@ 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;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.keyword.KeywordInterface;
|
||||||
import forge.game.phase.Untap;
|
import forge.game.phase.Untap;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.replacement.ReplacementEffect;
|
import forge.game.replacement.ReplacementEffect;
|
||||||
@@ -53,6 +49,8 @@ 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.MyRandom;
|
||||||
|
import forge.util.TextUtil;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
|
|
||||||
|
|
||||||
@@ -109,7 +107,8 @@ public class ComputerUtilCombat {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (final String keyword : atacker.getKeywords()) {
|
for (final KeywordInterface inst : atacker.getKeywords()) {
|
||||||
|
final String keyword = inst.getOriginal();
|
||||||
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
|
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
|
||||||
final String defined = keyword.split(":")[1];
|
final String defined = keyword.split(":")[1];
|
||||||
final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0);
|
final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0);
|
||||||
@@ -203,10 +202,10 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
|
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, combat, withoutAbilities);
|
||||||
if (!attacker.hasKeyword("Infect")) {
|
if (!attacker.hasKeyword(Keyword.INFECT)) {
|
||||||
sum = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
|
sum = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
|
||||||
if (attacker.hasKeyword("Double Strike")) {
|
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
sum += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
|
sum *= 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return sum;
|
return sum;
|
||||||
@@ -228,14 +227,15 @@ public class ComputerUtilCombat {
|
|||||||
int damage = attacker.getNetCombatDamage();
|
int damage = attacker.getNetCombatDamage();
|
||||||
int poison = 0;
|
int poison = 0;
|
||||||
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, null, false);
|
damage += ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, null, null, false);
|
||||||
if (attacker.hasKeyword("Infect")) {
|
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||||
poison += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
|
int pd = ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
|
||||||
if (attacker.hasKeyword("Double Strike")) {
|
poison += pd;
|
||||||
poison += ComputerUtilCombat.predictDamageTo(attacked, damage, attacker, true);
|
if (attacker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
|
poison += pd;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (attacker.hasKeyword("Poisonous") && (damage > 0)) {
|
if (attacker.hasKeyword(Keyword.POISONOUS) && (damage > 0)) {
|
||||||
poison += attacker.getKeywordMagnitude("Poisonous");
|
poison += attacker.getKeywordMagnitude(Keyword.POISONOUS);
|
||||||
}
|
}
|
||||||
return poison;
|
return poison;
|
||||||
}
|
}
|
||||||
@@ -303,9 +303,9 @@ public class ComputerUtilCombat {
|
|||||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage "
|
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage "
|
||||||
+ "as though it weren't blocked.")) {
|
+ "as though it weren't blocked.")) {
|
||||||
unblocked.add(attacker);
|
unblocked.add(attacker);
|
||||||
} else if (attacker.hasKeyword("Trample")
|
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
|
||||||
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
|
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
|
||||||
if (!attacker.hasKeyword("Infect")) {
|
if (!attacker.hasKeyword(Keyword.INFECT)) {
|
||||||
damage += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
|
damage += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -332,6 +332,11 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static int resultingPoison(final Player ai, final Combat combat) {
|
public static int resultingPoison(final Player ai, final Combat combat) {
|
||||||
|
|
||||||
|
// ai can't get poision counters, so the value can't change
|
||||||
|
if (!ai.canReceiveCounters(CounterType.POISON)) {
|
||||||
|
return ai.getPoisonCounters();
|
||||||
|
}
|
||||||
|
|
||||||
int poison = 0;
|
int poison = 0;
|
||||||
|
|
||||||
final List<Card> attackers = combat.getAttackersOf(ai);
|
final List<Card> attackers = combat.getAttackersOf(ai);
|
||||||
@@ -345,13 +350,13 @@ public class ComputerUtilCombat {
|
|||||||
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage"
|
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage"
|
||||||
+ " as though it weren't blocked.")) {
|
+ " as though it weren't blocked.")) {
|
||||||
unblocked.add(attacker);
|
unblocked.add(attacker);
|
||||||
} else if (attacker.hasKeyword("Trample")
|
} else if (attacker.hasKeyword(Keyword.TRAMPLE)
|
||||||
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
|
&& (ComputerUtilCombat.getAttack(attacker) > ComputerUtilCombat.totalShieldDamage(attacker, blockers))) {
|
||||||
if (attacker.hasKeyword("Infect")) {
|
if (attacker.hasKeyword(Keyword.INFECT)) {
|
||||||
poison += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
|
poison += ComputerUtilCombat.getAttack(attacker) - ComputerUtilCombat.totalShieldDamage(attacker, blockers);
|
||||||
}
|
}
|
||||||
if (attacker.hasKeyword("Poisonous")) {
|
if (attacker.hasKeyword(Keyword.POISONOUS)) {
|
||||||
poison += attacker.getKeywordMagnitude("Poisonous");
|
poison += attacker.getKeywordMagnitude(Keyword.POISONOUS);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -385,11 +390,27 @@ public class ComputerUtilCombat {
|
|||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
public static boolean lifeInDanger(final Player ai, final Combat combat) {
|
public static boolean lifeInDanger(final Player ai, final Combat combat) {
|
||||||
|
return lifeInDanger(ai, combat, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean lifeInDanger(final Player ai, final Combat combat, final int payment) {
|
||||||
// life in danger only cares about the player's life. Not Planeswalkers' life
|
// life in danger only cares about the player's life. Not Planeswalkers' life
|
||||||
if (ai.cantLose() || combat == null || combat.getAttackingPlayer() == ai) {
|
if (ai.cantLose() || combat == null || combat.getAttackingPlayer() == ai) {
|
||||||
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,15 +422,37 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
if (blockers.isEmpty()) {
|
if (blockers.isEmpty()) {
|
||||||
if (!attacker.getSVar("MustBeBlocked").equals("")) {
|
if (!attacker.getSVar("MustBeBlocked").equals("")) {
|
||||||
|
boolean cond = false;
|
||||||
|
String condVal = attacker.getSVar("MustBeBlocked");
|
||||||
|
boolean isAttackingPlayer = combat.getDefenderByAttacker(attacker) instanceof Player;
|
||||||
|
|
||||||
|
cond |= "true".equalsIgnoreCase(condVal);
|
||||||
|
cond |= "attackingplayer".equalsIgnoreCase(condVal) && isAttackingPlayer;
|
||||||
|
cond |= "attackingplayerconservative".equalsIgnoreCase(condVal) && isAttackingPlayer
|
||||||
|
&& ai.getCreaturesInPlay().size() >= 3 && ai.getCreaturesInPlay().size() > attacker.getController().getCreaturesInPlay().size();
|
||||||
|
|
||||||
|
if (cond) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
if (threateningCommanders.contains(attacker)) {
|
if (threateningCommanders.contains(attacker)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) < Math.min(4, ai.getLife())
|
int threshold = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD));
|
||||||
|
int maxTreshold = (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.AI_IN_DANGER_MAX_THRESHOLD)) - threshold;
|
||||||
|
|
||||||
|
int chance = MyRandom.getRandom().nextInt(80) + 5;
|
||||||
|
while (maxTreshold > 0) {
|
||||||
|
if (MyRandom.getRandom().nextInt(100) < chance) {
|
||||||
|
threshold++;
|
||||||
|
}
|
||||||
|
maxTreshold--;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) - payment < Math.min(threshold, ai.getLife())
|
||||||
&& !ai.cantLoseForZeroOrLessLife()) {
|
&& !ai.cantLoseForZeroOrLessLife()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -443,6 +486,10 @@ public class ComputerUtilCombat {
|
|||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat) {
|
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat) {
|
||||||
|
return lifeInSeriousDanger(ai, combat, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, final int payment) {
|
||||||
// life in danger only cares about the player's life. Not about a
|
// life in danger only cares about the player's life. Not about a
|
||||||
// Planeswalkers life
|
// Planeswalkers life
|
||||||
if (ai.cantLose() || combat == null) {
|
if (ai.cantLose() || combat == null) {
|
||||||
@@ -468,7 +515,7 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) < 1 && !ai.cantLoseForZeroOrLessLife()) {
|
if (ComputerUtilCombat.lifeThatWouldRemain(ai, combat) - payment < 1 && !ai.cantLoseForZeroOrLessLife()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,7 +583,7 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
|
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
|
||||||
|
|
||||||
if (defender.hasKeyword("Double Strike")) {
|
if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true);
|
defenderDamage += predictDamageTo(attacker, defenderDamage, defender, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,25 +597,26 @@ public class ComputerUtilCombat {
|
|||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
private static int predictDamageByBlockerWithoutDoubleStrike(final Card attacker, final Card defender) {
|
private static int predictDamageByBlockerWithoutDoubleStrike(final Card attacker, final Card defender) {
|
||||||
if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword("Indestructible")) {
|
if (attacker.getName().equals("Sylvan Basilisk") && !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
int flankingMagnitude = 0;
|
int flankingMagnitude = 0;
|
||||||
if (attacker.hasKeyword("Flanking") && !defender.hasKeyword("Flanking")) {
|
if (attacker.hasKeyword(Keyword.FLANKING) && !defender.hasKeyword(Keyword.FLANKING)) {
|
||||||
|
|
||||||
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
|
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
|
||||||
|
|
||||||
if (flankingMagnitude >= defender.getNetToughness()) {
|
if (flankingMagnitude >= defender.getNetToughness()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if ((flankingMagnitude >= (defender.getNetToughness() - defender.getDamage()))
|
if ((flankingMagnitude >= (defender.getNetToughness() - defender.getDamage()))
|
||||||
&& !defender.hasKeyword("Indestructible")) {
|
&& !defender.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // flanking
|
} // flanking
|
||||||
if (attacker.hasKeyword("Indestructible") && !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
|
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE)
|
||||||
|
&& !(defender.hasKeyword(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -627,21 +675,21 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int flankingMagnitude = 0;
|
int flankingMagnitude = 0;
|
||||||
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
|
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
|
||||||
|
|
||||||
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
|
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
|
||||||
|
|
||||||
if (flankingMagnitude >= blocker.getNetToughness()) {
|
if (flankingMagnitude >= blocker.getNetToughness()) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
|
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
|
||||||
&& !blocker.hasKeyword("Indestructible")) {
|
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // flanking
|
} // flanking
|
||||||
|
|
||||||
final int defBushidoMagnitude = blocker.getKeywordMagnitude("Bushido");
|
final int defBushidoMagnitude = blocker.getKeywordMagnitude(Keyword.BUSHIDO);
|
||||||
|
|
||||||
final int defenderDefense = (blocker.getLethalDamage() - flankingMagnitude) + defBushidoMagnitude;
|
final int defenderDefense = (blocker.getLethalDamage() - flankingMagnitude) + defBushidoMagnitude;
|
||||||
|
|
||||||
@@ -683,12 +731,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(Keyword.WITHER) || defender.hasKeyword(Keyword.INFECT))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (defender.hasKeyword(Keyword.FIRST_STRIKE) || defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
|
firstStrikeBlockerDmg += defender.getNetCombatDamage();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Consider first strike and double strike
|
||||||
|
if (attacker.hasKeyword(Keyword.FIRST_STRIKE) || attacker.hasKeyword(Keyword.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 +838,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 +852,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 +911,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")) {
|
||||||
@@ -868,14 +928,12 @@ public class ComputerUtilCombat {
|
|||||||
// if the attacker has first strike and wither the blocker will deal
|
// if the attacker has first strike and wither the blocker will deal
|
||||||
// less damage than expected
|
// less damage than expected
|
||||||
if (dealsFirstStrikeDamage(attacker, withoutAbilities, null)
|
if (dealsFirstStrikeDamage(attacker, withoutAbilities, null)
|
||||||
&& (attacker.hasKeyword("Wither") || attacker.hasKeyword("Infect"))
|
&& (attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))
|
||||||
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, null)
|
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, null)
|
||||||
&& !blocker.hasKeyword("CARDNAME can't have counters put on it.")) {
|
&& !blocker.canReceiveCounters(CounterType.M1M1)) {
|
||||||
power -= attacker.getNetCombatDamage();
|
power -= attacker.getNetCombatDamage();
|
||||||
}
|
}
|
||||||
|
|
||||||
power += blocker.getKeywordMagnitude("Bushido");
|
|
||||||
|
|
||||||
final Game game = attacker.getGame();
|
final Game game = attacker.getGame();
|
||||||
// look out for continuous static abilities that only care for blocking
|
// look out for continuous static abilities that only care for blocking
|
||||||
// creatures
|
// creatures
|
||||||
@@ -889,7 +947,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;
|
||||||
}
|
}
|
||||||
@@ -917,12 +975,20 @@ public class ComputerUtilCombat {
|
|||||||
final Map<String, String> trigParams = trigger.getMapParams();
|
final Map<String, String> trigParams = trigger.getMapParams();
|
||||||
final Card source = trigger.getHostCard();
|
final Card source = trigger.getHostCard();
|
||||||
|
|
||||||
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
|
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
|
||||||
|| !trigParams.containsKey("Execute")) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> abilityParams = null;
|
||||||
|
if (trigger.getOverridingAbility() != null) {
|
||||||
|
abilityParams = trigger.getOverridingAbility().getMapParams();
|
||||||
|
} else if (trigParams.containsKey("Execute")) {
|
||||||
final String ability = source.getSVar(trigParams.get("Execute"));
|
final String ability = source.getSVar(trigParams.get("Execute"));
|
||||||
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
|
abilityParams = AbilityFactory.getMapParams(ability);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump")) {
|
if (abilityParams.containsKey("AB") && !abilityParams.get("AB").equals("Pump")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -967,6 +1033,9 @@ public class ComputerUtilCombat {
|
|||||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ability.usesTargeting() && !ability.canTarget(blocker)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ability.getApi() == ApiType.Pump) {
|
if (ability.getApi() == ApiType.Pump) {
|
||||||
if (!ability.hasParam("NumAtt")) {
|
if (!ability.hasParam("NumAtt")) {
|
||||||
@@ -1016,15 +1085,14 @@ public class ComputerUtilCombat {
|
|||||||
public static int predictToughnessBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
|
public static int predictToughnessBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
|
||||||
int toughness = 0;
|
int toughness = 0;
|
||||||
|
|
||||||
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
|
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
|
||||||
toughness -= attacker.getAmountOfKeyword("Flanking");
|
toughness -= attacker.getAmountOfKeyword(Keyword.FLANKING);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blocker.getName().equals("Shape Stealer")) {
|
if (blocker.getName().equals("Shape Stealer")) {
|
||||||
toughness += attacker.getNetToughness() - blocker.getNetToughness();
|
toughness += attacker.getNetToughness() - blocker.getNetToughness();
|
||||||
}
|
}
|
||||||
|
|
||||||
toughness += blocker.getKeywordMagnitude("Bushido");
|
|
||||||
final Game game = attacker.getGame();
|
final Game game = attacker.getGame();
|
||||||
final FCollection<Trigger> theTriggers = new FCollection<Trigger>();
|
final FCollection<Trigger> theTriggers = new FCollection<Trigger>();
|
||||||
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
|
||||||
@@ -1038,12 +1106,20 @@ public class ComputerUtilCombat {
|
|||||||
final Map<String, String> trigParams = trigger.getMapParams();
|
final Map<String, String> trigParams = trigger.getMapParams();
|
||||||
final Card source = trigger.getHostCard();
|
final Card source = trigger.getHostCard();
|
||||||
|
|
||||||
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)
|
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, null)) {
|
||||||
|| !trigParams.containsKey("Execute")) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> abilityParams = null;
|
||||||
|
if (trigger.getOverridingAbility() != null) {
|
||||||
|
abilityParams = trigger.getOverridingAbility().getMapParams();
|
||||||
|
} else if (trigParams.containsKey("Execute")) {
|
||||||
final String ability = source.getSVar(trigParams.get("Execute"));
|
final String ability = source.getSVar(trigParams.get("Execute"));
|
||||||
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
|
abilityParams = AbilityFactory.getMapParams(ability);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
String abType = "";
|
String abType = "";
|
||||||
if (abilityParams.containsKey("AB")) {
|
if (abilityParams.containsKey("AB")) {
|
||||||
abType = abilityParams.get("AB");
|
abType = abilityParams.get("AB");
|
||||||
@@ -1129,6 +1205,9 @@ public class ComputerUtilCombat {
|
|||||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ability.usesTargeting() && !ability.canTarget(blocker)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ability.getApi() == ApiType.Pump) {
|
if (ability.getApi() == ApiType.Pump) {
|
||||||
if (!ability.hasParam("NumDef")) {
|
if (!ability.hasParam("NumDef")) {
|
||||||
@@ -1177,14 +1256,14 @@ public class ComputerUtilCombat {
|
|||||||
* @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");
|
|
||||||
//check Exalted only for the first attacker
|
//check Exalted only for the first attacker
|
||||||
if (combat != null && combat.getAttackers().isEmpty()) {
|
if (combat != null && combat.getAttackers().isEmpty()) {
|
||||||
for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
|
power += attacker.getController().countExaltedBonus();
|
||||||
power += card.getAmountOfKeyword("Exalted");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Serene Master switches power with attacker
|
// Serene Master switches power with attacker
|
||||||
@@ -1206,9 +1285,9 @@ public class ComputerUtilCombat {
|
|||||||
// less damage than expected
|
// less damage than expected
|
||||||
if (null != blocker) {
|
if (null != blocker) {
|
||||||
if (ComputerUtilCombat.dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
|
if (ComputerUtilCombat.dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
|
||||||
&& (blocker.hasKeyword("Wither") || blocker.hasKeyword("Infect"))
|
&& (blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT))
|
||||||
&& !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
|
&& !ComputerUtilCombat.dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
|
||||||
&& !attacker.hasKeyword("CARDNAME can't have counters put on it.")) {
|
&& !attacker.canReceiveCounters(CounterType.M1M1)) {
|
||||||
power -= blocker.getNetCombatDamage();
|
power -= blocker.getNetCombatDamage();
|
||||||
}
|
}
|
||||||
theTriggers.addAll(blocker.getTriggers());
|
theTriggers.addAll(blocker.getTriggers());
|
||||||
@@ -1216,6 +1295,7 @@ 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
|
||||||
|
if (!withoutCombatStaticAbilities) {
|
||||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||||
for (final Card card : cardList) {
|
for (final Card card : cardList) {
|
||||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
@@ -1226,7 +1306,7 @@ public class ComputerUtilCombat {
|
|||||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
|
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1241,17 +1321,26 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (final Trigger trigger : theTriggers) {
|
for (final Trigger trigger : theTriggers) {
|
||||||
final Map<String, String> trigParams = trigger.getMapParams();
|
final Map<String, String> trigParams = trigger.getMapParams();
|
||||||
final Card source = trigger.getHostCard();
|
final Card source = trigger.getHostCard();
|
||||||
|
|
||||||
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
|
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
|
||||||
|| !trigParams.containsKey("Execute")) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> abilityParams = null;
|
||||||
|
if (trigger.getOverridingAbility() != null) {
|
||||||
|
abilityParams = trigger.getOverridingAbility().getMapParams();
|
||||||
|
} else if (trigParams.containsKey("Execute")) {
|
||||||
final String ability = source.getSVar(trigParams.get("Execute"));
|
final String ability = source.getSVar(trigParams.get("Execute"));
|
||||||
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
|
abilityParams = AbilityFactory.getMapParams(ability);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
|
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
|
||||||
continue; // targeted pumping not supported
|
continue; // targeted pumping not supported
|
||||||
}
|
}
|
||||||
@@ -1265,7 +1354,14 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (abilityParams.containsKey("Cost")) {
|
if (abilityParams.containsKey("Cost")) {
|
||||||
final SpellAbility sa = AbilityFactory.getAbility(ability, source);
|
SpellAbility sa = null;
|
||||||
|
if (trigger.getOverridingAbility() != null) {
|
||||||
|
sa = trigger.getOverridingAbility();
|
||||||
|
} else {
|
||||||
|
final String ability = source.getSVar(trigParams.get("Execute"));
|
||||||
|
sa = AbilityFactory.getAbility(ability, source);
|
||||||
|
}
|
||||||
|
|
||||||
sa.setActivatingPlayer(source.getController());
|
sa.setActivatingPlayer(source.getController());
|
||||||
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
|
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1305,13 +1401,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
|
} else if (bonus.contains("TriggeredAttacker$CardPower")) { // e.g. Arahbo, Roar of the World
|
||||||
bonus = bonus.replace("TriggeredAttacker$CardPower", "Number$" + attacker.getNetPower());
|
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardPower", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetPower())));
|
||||||
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
|
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
|
||||||
bonus = bonus.replace("TriggeredAttacker$CardToughness", "Number$" + attacker.getNetToughness());
|
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness())));
|
||||||
}
|
}
|
||||||
power += CardFactoryUtil.xCount(source, bonus);
|
power += CardFactoryUtil.xCount(source, bonus);
|
||||||
|
|
||||||
@@ -1327,6 +1423,9 @@ public class ComputerUtilCombat {
|
|||||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ability.usesTargeting() && !ability.canTarget(attacker)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ability.getApi() == ApiType.Pump) {
|
if (ability.getApi() == ApiType.Pump) {
|
||||||
if (!ability.hasParam("NumAtt")) {
|
if (!ability.hasParam("NumAtt")) {
|
||||||
@@ -1376,13 +1475,15 @@ 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
|
||||||
if (combat != null && combat.getAttackers().isEmpty()) {
|
if (combat != null && combat.getAttackers().isEmpty()) {
|
||||||
for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
|
toughness += attacker.getController().countExaltedBonus();
|
||||||
toughness += card.getAmountOfKeyword("Exalted");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (blocker != null && attacker.getName().equals("Shape Stealer")) {
|
if (blocker != null && attacker.getName().equals("Shape Stealer")) {
|
||||||
@@ -1398,12 +1499,12 @@ public class ComputerUtilCombat {
|
|||||||
theTriggers.addAll(card.getTriggers());
|
theTriggers.addAll(card.getTriggers());
|
||||||
}
|
}
|
||||||
if (blocker != null) {
|
if (blocker != null) {
|
||||||
toughness += attacker.getKeywordMagnitude("Bushido");
|
|
||||||
theTriggers.addAll(blocker.getTriggers());
|
theTriggers.addAll(blocker.getTriggers());
|
||||||
}
|
}
|
||||||
|
|
||||||
// look out for continuous static abilities that only care for attacking
|
// look out for continuous static abilities that only care for attacking
|
||||||
// creatures
|
// creatures
|
||||||
|
if (!withoutCombatStaticAbilities) {
|
||||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||||
for (final Card card : cardList) {
|
for (final Card card : cardList) {
|
||||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
@@ -1412,7 +1513,7 @@ public class ComputerUtilCombat {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) {
|
if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) {
|
||||||
final String valid = params.get("Affected").replace("attacking", "Creature");
|
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1426,9 +1527,9 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
|
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
|
||||||
final String valid = params.get("Affected").replace("untapped", "Creature");
|
final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature");
|
||||||
if (!attacker.isValid(valid, card.getController(), card, null)
|
if (!attacker.isValid(valid, card.getController(), card, null)
|
||||||
|| attacker.hasKeyword("Vigilance")) {
|
|| attacker.hasKeyword(Keyword.VIGILANCE)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
// remove the bonus, because it will no longer be granted
|
// remove the bonus, because it will no longer be granted
|
||||||
@@ -1438,17 +1539,26 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (final Trigger trigger : theTriggers) {
|
for (final Trigger trigger : theTriggers) {
|
||||||
final Map<String, String> trigParams = trigger.getMapParams();
|
final Map<String, String> trigParams = trigger.getMapParams();
|
||||||
final Card source = trigger.getHostCard();
|
final Card source = trigger.getHostCard();
|
||||||
|
|
||||||
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)
|
if (!ComputerUtilCombat.combatTriggerWillTrigger(attacker, blocker, trigger, combat)) {
|
||||||
|| !trigParams.containsKey("Execute")) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, String> abilityParams = null;
|
||||||
|
if (trigger.getOverridingAbility() != null) {
|
||||||
|
abilityParams = trigger.getOverridingAbility().getMapParams();
|
||||||
|
} else if (trigParams.containsKey("Execute")) {
|
||||||
final String ability = source.getSVar(trigParams.get("Execute"));
|
final String ability = source.getSVar(trigParams.get("Execute"));
|
||||||
final Map<String, String> abilityParams = AbilityFactory.getMapParams(ability);
|
abilityParams = AbilityFactory.getMapParams(ability);
|
||||||
|
} else {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
|
if (abilityParams.containsKey("ValidTgts") || abilityParams.containsKey("Tgt")) {
|
||||||
continue; // targeted pumping not supported
|
continue; // targeted pumping not supported
|
||||||
}
|
}
|
||||||
@@ -1481,7 +1591,14 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (abilityParams.containsKey("Cost")) {
|
if (abilityParams.containsKey("Cost")) {
|
||||||
final SpellAbility sa = AbilityFactory.getAbility(ability, source);
|
SpellAbility sa = null;
|
||||||
|
if (trigger.getOverridingAbility() != null) {
|
||||||
|
sa = trigger.getOverridingAbility();
|
||||||
|
} else {
|
||||||
|
final String ability = source.getSVar(trigParams.get("Execute"));
|
||||||
|
sa = AbilityFactory.getAbility(ability, source);
|
||||||
|
}
|
||||||
|
|
||||||
sa.setActivatingPlayer(source.getController());
|
sa.setActivatingPlayer(source.getController());
|
||||||
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
|
if (!CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa)) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1521,9 +1638,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);
|
||||||
}
|
}
|
||||||
@@ -1539,6 +1656,9 @@ public class ComputerUtilCombat {
|
|||||||
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
if (ability.hasParam("ActivationPhases") || ability.hasParam("SorcerySpeed") || ability.hasParam("ActivationZone")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
if (ability.usesTargeting() && !ability.canTarget(attacker)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (ability.getApi() == ApiType.Pump) {
|
if (ability.getApi() == ApiType.Pump) {
|
||||||
if (!ability.hasParam("NumDef")) {
|
if (!ability.hasParam("NumDef")) {
|
||||||
@@ -1577,7 +1697,7 @@ public class ComputerUtilCombat {
|
|||||||
if (blocker.isEquippedBy("Godsend")) {
|
if (blocker.isEquippedBy("Godsend")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (attacker.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(attacker.getController(), attacker)) {
|
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(attacker.getController(), attacker)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1642,12 +1762,12 @@ public class ComputerUtilCombat {
|
|||||||
*/
|
*/
|
||||||
public static boolean attackerCantBeDestroyedInCombat(Player ai, final Card attacker) {
|
public static boolean attackerCantBeDestroyedInCombat(Player ai, final Card attacker) {
|
||||||
// attacker is either indestructible or may regenerate
|
// attacker is either indestructible or may regenerate
|
||||||
if (attacker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, attacker))) {
|
if (attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// attacker will regenerate
|
// attacker will regenerate
|
||||||
if (attacker.getShieldCount() > 0 && !attacker.hasKeyword("CARDNAME can't be regenerated.")) {
|
if (attacker.getShieldCount() > 0 && attacker.canBeShielded()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1678,6 +1798,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);
|
||||||
@@ -1692,24 +1816,24 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int flankingMagnitude = 0;
|
int flankingMagnitude = 0;
|
||||||
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
|
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
|
||||||
|
|
||||||
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
|
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
|
||||||
|
|
||||||
if (flankingMagnitude >= blocker.getNetToughness()) {
|
if (flankingMagnitude >= blocker.getNetToughness()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
|
if ((flankingMagnitude >= (blocker.getNetToughness() - blocker.getDamage()))
|
||||||
&& !blocker.hasKeyword("Indestructible")) {
|
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} // flanking
|
} // flanking
|
||||||
|
|
||||||
if (((attacker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities))
|
if (((attacker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, attacker) && !withoutAbilities))
|
||||||
&& !(blocker.hasKeyword("Wither") || blocker.hasKeyword("Infect")))
|
&& !(blocker.hasKeyword(Keyword.WITHER) || blocker.hasKeyword(Keyword.INFECT)))
|
||||||
|| (attacker.hasKeyword("Persist") && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker
|
|| (attacker.hasKeyword(Keyword.PERSIST) && !attacker.canReceiveCounters(CounterType.M1M1) && (attacker
|
||||||
.getCounters(CounterType.M1M1) == 0))
|
.getCounters(CounterType.M1M1) == 0))
|
||||||
|| (attacker.hasKeyword("Undying") && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker
|
|| (attacker.hasKeyword(Keyword.UNDYING) && !attacker.canReceiveCounters(CounterType.P1P1) && (attacker
|
||||||
.getCounters(CounterType.P1P1) == 0))) {
|
.getCounters(CounterType.P1P1) == 0))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1729,10 +1853,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;
|
||||||
@@ -1754,9 +1878,9 @@ 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 (blocker.hasKeyword("Double Strike")) {
|
if (blocker.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1766,7 +1890,8 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
// Attacker may kill the blocker before he can deal normal
|
// Attacker may kill the blocker before he can deal normal
|
||||||
// (secondary) damage
|
// (secondary) damage
|
||||||
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat) && !blocker.hasKeyword("Indestructible")) {
|
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
|
||||||
|
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
if (attackerDamage >= defenderLife) {
|
if (attackerDamage >= defenderLife) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1782,7 +1907,7 @@ public class ComputerUtilCombat {
|
|||||||
else { // no double strike for defender
|
else { // no double strike for defender
|
||||||
// Attacker may kill the blocker before he can deal any damage
|
// Attacker may kill the blocker before he can deal any damage
|
||||||
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
|
if (dealsFirstStrikeDamage(attacker, withoutAbilities, combat)
|
||||||
&& !blocker.hasKeyword("Indestructible")
|
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)
|
||||||
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, combat)) {
|
&& !dealsFirstStrikeDamage(blocker, withoutAbilities, combat)) {
|
||||||
|
|
||||||
if (attackerDamage >= defenderLife) {
|
if (attackerDamage >= defenderLife) {
|
||||||
@@ -1821,7 +1946,7 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
for (Card attacker : attackers) {
|
for (Card attacker : attackers) {
|
||||||
if (ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, true)
|
if (ComputerUtilCombat.canDestroyBlocker(ai, blocker, attacker, combat, true)
|
||||||
&& !(attacker.hasKeyword("Wither") || attacker.hasKeyword("Infect"))) {
|
&& !(attacker.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1839,19 +1964,21 @@ public class ComputerUtilCombat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
int flankingMagnitude = 0;
|
int flankingMagnitude = 0;
|
||||||
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
|
if (attacker.hasKeyword(Keyword.FLANKING) && !blocker.hasKeyword(Keyword.FLANKING)) {
|
||||||
|
|
||||||
flankingMagnitude = attacker.getAmountOfKeyword("Flanking");
|
flankingMagnitude = attacker.getAmountOfKeyword(Keyword.FLANKING);
|
||||||
|
|
||||||
if (flankingMagnitude >= blocker.getNetToughness()) {
|
if (flankingMagnitude >= blocker.getNetToughness()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((flankingMagnitude >= ComputerUtilCombat.getDamageToKill(blocker)) && !blocker.hasKeyword("Indestructible")) {
|
if ((flankingMagnitude >= ComputerUtilCombat.getDamageToKill(blocker))
|
||||||
|
&& !blocker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
} // flanking
|
} // flanking
|
||||||
|
|
||||||
if (blocker.hasKeyword("Indestructible") || dontTestRegen || ComputerUtil.canRegenerate(blocker.getController(), blocker)) {
|
if (blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || dontTestRegen
|
||||||
|
|| ComputerUtil.canRegenerate(blocker.getController(), blocker)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1923,6 +2050,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);
|
||||||
@@ -1932,11 +2063,11 @@ public class ComputerUtilCombat {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (((blocker.hasKeyword("Indestructible") || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker
|
if (((blocker.hasKeyword(Keyword.INDESTRUCTIBLE) || (ComputerUtil.canRegenerate(ai, blocker) && !withoutAbilities)) && !(attacker
|
||||||
.hasKeyword("Wither") || attacker.hasKeyword("Infect")))
|
.hasKeyword(Keyword.WITHER) || attacker.hasKeyword(Keyword.INFECT)))
|
||||||
|| (blocker.hasKeyword("Persist") && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker
|
|| (blocker.hasKeyword(Keyword.PERSIST) && !blocker.canReceiveCounters(CounterType.M1M1) && (blocker
|
||||||
.getCounters(CounterType.M1M1) == 0))
|
.getCounters(CounterType.M1M1) == 0))
|
||||||
|| (blocker.hasKeyword("Undying") && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker
|
|| (blocker.hasKeyword(Keyword.UNDYING) && !blocker.canReceiveCounters(CounterType.P1P1) && (blocker
|
||||||
.getCounters(CounterType.P1P1) == 0))) {
|
.getCounters(CounterType.P1P1) == 0))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1956,10 +2087,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;
|
||||||
@@ -1984,9 +2115,9 @@ 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(Keyword.DOUBLE_STRIKE)) {
|
||||||
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
|
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1996,7 +2127,8 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
// Attacker may kill the blocker before he can deal normal
|
// Attacker may kill the blocker before he can deal normal
|
||||||
// (secondary) damage
|
// (secondary) damage
|
||||||
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible")) {
|
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
|
||||||
|
&& !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
if (defenderDamage >= attackerLife) {
|
if (defenderDamage >= attackerLife) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -2011,7 +2143,8 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
else { // no double strike for attacker
|
else { // no double strike for attacker
|
||||||
// Defender may kill the attacker before he can deal any damage
|
// Defender may kill the attacker before he can deal any damage
|
||||||
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat) && !attacker.hasKeyword("Indestructible")
|
if (dealsFirstStrikeDamage(blocker, withoutAbilities, combat)
|
||||||
|
&& !attacker.hasKeyword(Keyword.INDESTRUCTIBLE)
|
||||||
&& !dealsFirstStrikeDamage(attacker, withoutAbilities, combat)) {
|
&& !dealsFirstStrikeDamage(attacker, withoutAbilities, combat)) {
|
||||||
|
|
||||||
if (defenderDamage >= attackerLife) {
|
if (defenderDamage >= attackerLife) {
|
||||||
@@ -2058,7 +2191,7 @@ public class ComputerUtilCombat {
|
|||||||
return damageMap;
|
return damageMap;
|
||||||
}
|
}
|
||||||
|
|
||||||
final boolean hasTrample = attacker.hasKeyword("Trample");
|
final boolean hasTrample = attacker.hasKeyword(Keyword.TRAMPLE);
|
||||||
|
|
||||||
if (block.size() == 1) {
|
if (block.size() == 1) {
|
||||||
final Card blocker = block.getFirst();
|
final Card blocker = block.getFirst();
|
||||||
@@ -2160,11 +2293,11 @@ public class ComputerUtilCombat {
|
|||||||
final boolean noPrevention) {
|
final boolean noPrevention) {
|
||||||
final int killDamage = c.isPlaneswalker() ? c.getCurrentLoyalty() : ComputerUtilCombat.getDamageToKill(c);
|
final int killDamage = c.isPlaneswalker() ? c.getCurrentLoyalty() : ComputerUtilCombat.getDamageToKill(c);
|
||||||
|
|
||||||
if (c.hasKeyword("Indestructible") || c.getShieldCount() > 0) {
|
if (c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getShieldCount() > 0) {
|
||||||
if (!(source.hasKeyword("Wither") || source.hasKeyword("Infect"))) {
|
if (!(source.hasKeyword(Keyword.WITHER) || source.hasKeyword(Keyword.INFECT))) {
|
||||||
return maxDamage + 1;
|
return maxDamage + 1;
|
||||||
}
|
}
|
||||||
} else if (source.hasKeyword("Deathtouch")) {
|
} else if (source.hasKeyword(Keyword.DEATHTOUCH)) {
|
||||||
for (int i = 1; i <= maxDamage; i++) {
|
for (int i = 1; i <= maxDamage; i++) {
|
||||||
if (noPrevention) {
|
if (noPrevention) {
|
||||||
if (c.staticReplaceDamage(i, source, isCombat) > 0) {
|
if (c.staticReplaceDamage(i, source, isCombat) > 0) {
|
||||||
@@ -2201,6 +2334,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();
|
||||||
@@ -2326,7 +2460,7 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
|
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
|
||||||
|
|
||||||
if (combatant.hasKeyword("Double Strike") || combatant.hasKeyword("First Strike")) {
|
if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2407,7 +2541,7 @@ public class ComputerUtilCombat {
|
|||||||
for (SpellAbility sa : original.getSpellAbilities()) {
|
for (SpellAbility sa : original.getSpellAbilities()) {
|
||||||
if (sa.getApi() == ApiType.SetState && ComputerUtilCost.canPayCost(sa, original.getController())) {
|
if (sa.getApi() == ApiType.SetState && ComputerUtilCost.canPayCost(sa, original.getController())) {
|
||||||
Card transformed = CardUtil.getLKICopy(original);
|
Card transformed = CardUtil.getLKICopy(original);
|
||||||
transformed.getCurrentState().copyFrom(original, original.getAlternateState());
|
transformed.getCurrentState().copyFrom(original.getAlternateState(), true);
|
||||||
transformed.updateStateForView();
|
transformed.updateStateForView();
|
||||||
return transformed;
|
return transformed;
|
||||||
}
|
}
|
||||||
@@ -2437,7 +2571,7 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) {
|
public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) {
|
||||||
// TODO: expand this to account for more complex situations like the Wildfire Eternal unblocked trigger
|
// TODO: expand this to account for more complex situations like the Wildfire Eternal unblocked trigger
|
||||||
int afflictDmg = attacker.getKeywordMagnitude("Afflict");
|
int afflictDmg = attacker.getKeywordMagnitude(Keyword.AFFLICT);
|
||||||
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
|
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2454,6 +2588,73 @@ public class ComputerUtilCombat {
|
|||||||
|
|
||||||
return -1;
|
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) {
|
||||||
|
if (atk.hasKeyword(Keyword.FLYING) || atk.hasKeyword(Keyword.SHADOW)
|
||||||
|
|| atk.hasKeyword(Keyword.HORSEMANSHIP) || (atk.hasKeyword(Keyword.FEAR)
|
||||||
|
|| atk.hasKeyword(Keyword.INTIMIDATE) || atk.hasKeyword(Keyword.SKULK)
|
||||||
|
|| atk.hasKeyword(Keyword.PROTECTION))) {
|
||||||
|
withEvasion.add(atk);
|
||||||
|
} else {
|
||||||
|
withoutEvasion.add(atk);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// attackers that can only be blocked by cards with specific keywords or color, etc.
|
||||||
|
// (maybe will need to split into 2 or 3 tiers depending on importance)
|
||||||
|
categorizedAttackers.addAll(withEvasion);
|
||||||
|
// all other attackers that have no evasion
|
||||||
|
// (Menace and other abilities that limit blocking by amount of blockers is likely handled
|
||||||
|
// elsewhere, but that needs testing and possibly fine-tuning).
|
||||||
|
categorizedAttackers.addAll(withoutEvasion);
|
||||||
|
|
||||||
|
return categorizedAttackers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Card applyPotentialAttackCloneTriggers(Card attacker) {
|
||||||
|
// This method returns the potentially cloned card if the creature turns into something else during the attack
|
||||||
|
// (currently looks for the creature with maximum raw power since that's what the AI usually judges by when
|
||||||
|
// deciding whether the creature is worth blocking).
|
||||||
|
// If the creature doesn't change into anything, returns the original creature.
|
||||||
|
if (attacker == null) { return null; }
|
||||||
|
Card attackerAfterTrigs = attacker;
|
||||||
|
|
||||||
|
// Test for some special triggers that can change the creature in combat
|
||||||
|
for (Trigger t : attacker.getTriggers()) {
|
||||||
|
if (t.getMode() == TriggerType.Attacks && t.hasParam("Execute")) {
|
||||||
|
if (!attacker.hasSVar(t.getParam("Execute"))) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
SpellAbility exec = AbilityFactory.getAbility(attacker, t.getParam("Execute"));
|
||||||
|
if (exec != null) {
|
||||||
|
if (exec.getApi() == ApiType.Clone && "Self".equals(exec.getParam("CloneTarget"))
|
||||||
|
&& exec.hasParam("ValidTgts") && exec.getParam("ValidTgts").contains("Creature")
|
||||||
|
&& exec.getParam("ValidTgts").contains("attacking")) {
|
||||||
|
// Tilonalli's Skinshifter and potentially other similar cards that can clone other stuff
|
||||||
|
// while attacking
|
||||||
|
if (exec.getParam("ValidTgts").contains("nonLegendary") && attacker.getType().isLegendary()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
int maxPwr = 0;
|
||||||
|
for (Card c : attacker.getController().getCreaturesInPlay()) {
|
||||||
|
if (c.getNetPower() > maxPwr || (c.getNetPower() == maxPwr && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(attackerAfterTrigs))) {
|
||||||
|
maxPwr = c.getNetPower();
|
||||||
|
attackerAfterTrigs = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return attackerAfterTrigs;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,15 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Sets;
|
import com.google.common.collect.Sets;
|
||||||
|
|
||||||
import forge.ai.ability.AnimateAi;
|
import forge.ai.ability.AnimateAi;
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
import forge.game.GameActionUtil;
|
import forge.game.GameActionUtil;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
|
import forge.game.card.CardFactoryUtil;
|
||||||
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.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
@@ -27,6 +22,10 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.TextUtil;
|
import forge.util.TextUtil;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
|
||||||
public class ComputerUtilCost {
|
public class ComputerUtilCost {
|
||||||
@@ -90,6 +89,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 +239,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;
|
||||||
}
|
}
|
||||||
@@ -247,10 +257,19 @@ public class ComputerUtilCost {
|
|||||||
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);
|
||||||
|
|
||||||
|
int count = 0;
|
||||||
|
while (count < amount) {
|
||||||
|
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
|
||||||
|
if (prefCard == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
sacList.add(prefCard);
|
||||||
|
typeList.remove(prefCard);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -267,13 +286,14 @@ 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();
|
||||||
|
|
||||||
@@ -286,10 +306,20 @@ 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) {
|
|
||||||
|
int count = 0;
|
||||||
|
while (count < amount) {
|
||||||
|
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
|
||||||
|
if (prefCard == null) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
sacList.add(prefCard);
|
||||||
|
typeList.remove(prefCard);
|
||||||
|
count++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
@@ -336,7 +366,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 +394,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -417,7 +447,7 @@ public class ComputerUtilCost {
|
|||||||
// Check for stuff like Nether Void
|
// Check for stuff like Nether Void
|
||||||
int extraManaNeeded = 0;
|
int extraManaNeeded = 0;
|
||||||
if (sa instanceof Spell) {
|
if (sa instanceof Spell) {
|
||||||
final boolean cannotBeCountered = sa.getHostCard().hasKeyword("CARDNAME can't be countered.");
|
final boolean cannotBeCountered = !CardFactoryUtil.isCounterable(sa.getHostCard());
|
||||||
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
|
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
|
||||||
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
|
||||||
if (!StringUtils.isBlank(snem)) {
|
if (!StringUtils.isBlank(snem)) {
|
||||||
@@ -561,7 +591,7 @@ 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)
|
||||||
@@ -578,7 +608,6 @@ public class ComputerUtilCost {
|
|||||||
Set<String> colorsAvailable = Sets.newHashSet();
|
Set<String> colorsAvailable = Sets.newHashSet();
|
||||||
|
|
||||||
if (additionalLands != null) {
|
if (additionalLands != null) {
|
||||||
GameActionUtil.grantBasicLandsManaAbilities(additionalLands);
|
|
||||||
cardsToConsider.addAll(additionalLands);
|
cardsToConsider.addAll(additionalLands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,15 +14,12 @@ 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;
|
||||||
import forge.game.cost.CostPartMana;
|
import forge.game.cost.CostPartMana;
|
||||||
|
import forge.game.cost.CostPayEnergy;
|
||||||
import forge.game.cost.CostPayment;
|
import forge.game.cost.CostPayment;
|
||||||
import forge.game.mana.Mana;
|
import forge.game.mana.Mana;
|
||||||
import forge.game.mana.ManaCostBeingPaid;
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
@@ -41,7 +34,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,7 +195,8 @@ public class ComputerUtilMana {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.getHostCard() != null && sa.getApi() == ApiType.Animate) {
|
if (sa.getHostCard() != null) {
|
||||||
|
if (sa.getApi() == ApiType.Animate) {
|
||||||
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
|
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
|
||||||
if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
|
if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
|
||||||
&& ma.getHostCard() == sa.getHostCard().getEnchantingCard()
|
&& ma.getHostCard() == sa.getHostCard().getEnchantingCard()
|
||||||
@@ -213,10 +206,31 @@ public class ComputerUtilMana {
|
|||||||
|
|
||||||
// If a manland was previously animated this turn, do not tap it to animate another manland
|
// If a manland was previously animated this turn, do not tap it to animate another manland
|
||||||
if (sa.getHostCard().isLand() && ma.getHostCard().isLand()
|
if (sa.getHostCard().isLand() && ma.getHostCard().isLand()
|
||||||
&& ai.getController() instanceof PlayerControllerAi
|
&& ai.getController().isAI()
|
||||||
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
|
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
} else if (sa.getApi() == ApiType.Pump) {
|
||||||
|
if ((sa.getHostCard().isInstant() || sa.getHostCard().isSorcery())
|
||||||
|
&& ma.getHostCard().isCreature()
|
||||||
|
&& ai.getController().isAI()
|
||||||
|
&& ma.getPayCosts().hasTapCost()
|
||||||
|
&& sa.getTargets().getTargetCards().contains(ma.getHostCard())) {
|
||||||
|
// do not activate pump instants/sorceries targeting creatures by tapping targeted
|
||||||
|
// creatures for mana (for example, Servant of the Conduit)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if (sa.getApi() == ApiType.Attach
|
||||||
|
&& "AvoidPayingWithAttachTarget".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
|
||||||
|
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
|
||||||
|
// be tapped to pay its own cost if there's another untapped land like that available
|
||||||
|
if (ma.getHostCard().equals(sa.getTargetCard())) {
|
||||||
|
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.nameEquals(ma.getHostCard().getName()), CardPredicates.Presets.UNTAPPED)).size() > 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
SpellAbility paymentChoice = ma;
|
SpellAbility paymentChoice = ma;
|
||||||
@@ -231,12 +245,14 @@ public class ComputerUtilMana {
|
|||||||
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
|
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
|
||||||
for (SpellAbility ab : saList) {
|
for (SpellAbility ab : saList) {
|
||||||
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
|
||||||
|
if (!ab.getHostCard().isTapped()) {
|
||||||
paymentChoice = ab;
|
paymentChoice = ab;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final String typeRes = cost.getSourceRestriction();
|
final String typeRes = cost.getSourceRestriction();
|
||||||
if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().getType().hasStringType(typeRes)) {
|
if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().getType().hasStringType(typeRes)) {
|
||||||
@@ -345,6 +361,7 @@ public class ComputerUtilMana {
|
|||||||
adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai);
|
adjustManaCostToAvoidNegEffects(cost, sa.getHostCard(), ai);
|
||||||
List<Mana> manaSpentToPay = test ? new ArrayList<Mana>() : sa.getPayingMana();
|
List<Mana> manaSpentToPay = test ? new ArrayList<Mana>() : sa.getPayingMana();
|
||||||
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
|
||||||
|
int testEnergyPool = ai.getCounters(CounterType.ENERGY);
|
||||||
|
|
||||||
List<SpellAbility> paymentList = Lists.newArrayList();
|
List<SpellAbility> paymentList = Lists.newArrayList();
|
||||||
|
|
||||||
@@ -443,6 +460,16 @@ public class ComputerUtilMana {
|
|||||||
setExpressColorChoice(sa, ai, cost, toPay, saPayment);
|
setExpressColorChoice(sa, ai, cost, toPay, saPayment);
|
||||||
|
|
||||||
if (test) {
|
if (test) {
|
||||||
|
// Check energy when testing
|
||||||
|
CostPayEnergy energyCost = saPayment.getPayCosts().getCostEnergy();
|
||||||
|
if (energyCost != null) {
|
||||||
|
testEnergyPool -= Integer.parseInt(energyCost.getAmount());
|
||||||
|
if (testEnergyPool < 0) {
|
||||||
|
// Can't pay energy cost
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String manaProduced = toPay.isSnow() ? "S" : GameActionUtil.generatedMana(saPayment);
|
String manaProduced = toPay.isSnow() ? "S" : GameActionUtil.generatedMana(saPayment);
|
||||||
manaProduced = AbilityManaPart.applyManaReplacement(saPayment, manaProduced);
|
manaProduced = AbilityManaPart.applyManaReplacement(saPayment, manaProduced);
|
||||||
//System.out.println(manaProduced);
|
//System.out.println(manaProduced);
|
||||||
@@ -498,6 +525,16 @@ public class ComputerUtilMana {
|
|||||||
// extraMana, sa.getHostCard(), sa.toUnsuppressedString(), StringUtils.join(paymentPlan, "\n\t"));
|
// extraMana, sa.getHostCard(), sa.toUnsuppressedString(), StringUtils.join(paymentPlan, "\n\t"));
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// See if it's possible to pay with something that was left in the mana pool in corner cases,
|
||||||
|
// e.g. Gemstone Caverns with a Luck counter on it generating colored mana (which fails to be
|
||||||
|
// processed correctly on a per-ability basis, leaving floating mana in the pool)
|
||||||
|
if (!cost.isPaid() && !manapool.isEmpty()) {
|
||||||
|
for (byte color : MagicColor.WUBRGC) {
|
||||||
|
manapool.tryPayCostWithColor(color, sa, cost);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The cost is still unpaid, so refund the mana and report
|
||||||
if (!cost.isPaid()) {
|
if (!cost.isPaid()) {
|
||||||
refundMana(manaSpentToPay, ai, sa);
|
refundMana(manaSpentToPay, ai, sa);
|
||||||
if (test) {
|
if (test) {
|
||||||
@@ -809,7 +846,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) {
|
||||||
@@ -823,8 +860,24 @@ 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 (!(ai.getGame().getPhaseHandler().isPlayerTurn(ai)) && (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP)) {
|
||||||
|
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
|
||||||
|
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
|
||||||
|
} else {
|
||||||
|
if ((AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) ||
|
||||||
|
(AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_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) {
|
||||||
@@ -832,16 +885,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1040,7 +1093,7 @@ public class ComputerUtilMana {
|
|||||||
ZoneType castFromBackup = null;
|
ZoneType castFromBackup = null;
|
||||||
if (test && sa.isSpell()) {
|
if (test && sa.isSpell()) {
|
||||||
castFromBackup = card.getCastFrom();
|
castFromBackup = card.getCastFrom();
|
||||||
sa.getHostCard().setCastFrom(card.getZone().getZoneType());
|
sa.getHostCard().setCastFrom(card.getZone() != null ? card.getZone().getZoneType() : null);
|
||||||
}
|
}
|
||||||
|
|
||||||
Cost payCosts = CostAdjustment.adjust(sa.getPayCosts(), sa);
|
Cost payCosts = CostAdjustment.adjust(sa.getPayCosts(), sa);
|
||||||
@@ -1065,7 +1118,9 @@ public class ComputerUtilMana {
|
|||||||
final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
|
final String xSvar = card.getSVar("X").startsWith("Count$xPaid") ? "PayX" : "X";
|
||||||
if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar)) {
|
if (!sa.getSVar(xSvar).isEmpty() || card.hasSVar(xSvar)) {
|
||||||
if (xSvar.equals("PayX") && card.hasSVar(xSvar)) {
|
if (xSvar.equals("PayX") && card.hasSVar(xSvar)) {
|
||||||
manaToAdd = Integer.parseInt(card.getSVar(xSvar)) * cost.getXcounter(); // X
|
// X SVar may end up being an empty string when copying a spell with no cost (e.g. Jhoira Avatar)
|
||||||
|
String xValue = card.getSVar(xSvar);
|
||||||
|
manaToAdd = xValue.isEmpty() ? 0 : Integer.parseInt(card.getSVar(xSvar)) * cost.getXcounter(); // X
|
||||||
} else {
|
} else {
|
||||||
manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter();
|
manaToAdd = AbilityUtils.calculateAmount(card, xSvar, sa) * cost.getXcounter();
|
||||||
}
|
}
|
||||||
@@ -1099,8 +1154,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
|
||||||
@@ -1207,7 +1319,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) {
|
||||||
@@ -1228,7 +1340,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);
|
||||||
}
|
}
|
||||||
@@ -1271,7 +1383,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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,13 @@ package forge.ai;
|
|||||||
|
|
||||||
import com.google.common.base.Function;
|
import com.google.common.base.Function;
|
||||||
|
|
||||||
|
import forge.game.ability.AbilityUtils;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CounterType;
|
||||||
|
import forge.game.cost.CostPayEnergy;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
|
import forge.game.keyword.KeywordInterface;
|
||||||
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,13 +25,18 @@ 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
|
||||||
}
|
}
|
||||||
int power = getEffectivePower(c);
|
int power = getEffectivePower(c);
|
||||||
final int toughness = getEffectiveToughness(c);
|
final int toughness = getEffectiveToughness(c);
|
||||||
for (String keyword : c.getKeywords()) {
|
for (KeywordInterface kw : c.getKeywords()) {
|
||||||
|
String keyword = kw.getOriginal();
|
||||||
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|
if (keyword.equals("Prevent all combat damage that would be dealt by CARDNAME.")
|
||||||
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|
|| keyword.equals("Prevent all damage that would be dealt by CARDNAME.")
|
||||||
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
|| keyword.equals("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|
||||||
@@ -34,15 +45,19 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (considerPT) {
|
||||||
value += addValue(power * 15, "power");
|
value += addValue(power * 15, "power");
|
||||||
value += addValue(toughness * 10, "toughness: " + toughness);
|
value += addValue(toughness * 10, "toughness: " + toughness);
|
||||||
|
}
|
||||||
|
if (considerCMC) {
|
||||||
value += addValue(c.getCMC() * 5, "cmc");
|
value += addValue(c.getCMC() * 5, "cmc");
|
||||||
|
}
|
||||||
|
|
||||||
// Evasion keywords
|
// Evasion keywords
|
||||||
if (c.hasKeyword("Flying")) {
|
if (c.hasKeyword(Keyword.FLYING)) {
|
||||||
value += addValue(power * 10, "flying");
|
value += addValue(power * 10, "flying");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Horsemanship")) {
|
if (c.hasKeyword(Keyword.HORSEMANSHIP)) {
|
||||||
value += addValue(power * 10, "horses");
|
value += addValue(power * 10, "horses");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Unblockable")) {
|
if (c.hasKeyword("Unblockable")) {
|
||||||
@@ -51,13 +66,13 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
if (c.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
|
||||||
value += addValue(power * 6, "thorns");
|
value += addValue(power * 6, "thorns");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Fear")) {
|
if (c.hasKeyword(Keyword.FEAR)) {
|
||||||
value += addValue(power * 6, "fear");
|
value += addValue(power * 6, "fear");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Intimidate")) {
|
if (c.hasKeyword(Keyword.INTIMIDATE)) {
|
||||||
value += addValue(power * 6, "intimidate");
|
value += addValue(power * 6, "intimidate");
|
||||||
}
|
}
|
||||||
if (c.hasStartOfKeyword("Menace")) {
|
if (c.hasKeyword(Keyword.MENACE)) {
|
||||||
value += addValue(power * 4, "menace");
|
value += addValue(power * 4, "menace");
|
||||||
}
|
}
|
||||||
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
|
if (c.hasStartOfKeyword("CantBeBlockedBy")) {
|
||||||
@@ -67,40 +82,49 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
|
|
||||||
// Other good keywords
|
// Other good keywords
|
||||||
if (power > 0) {
|
if (power > 0) {
|
||||||
if (c.hasKeyword("Double Strike")) {
|
if (c.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
value += addValue(10 + (power * 15), "ds");
|
value += addValue(10 + (power * 15), "ds");
|
||||||
} else if (c.hasKeyword("First Strike")) {
|
} else if (c.hasKeyword(Keyword.FIRST_STRIKE)) {
|
||||||
value += addValue(10 + (power * 5), "fs");
|
value += addValue(10 + (power * 5), "fs");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Deathtouch")) {
|
if (c.hasKeyword(Keyword.DEATHTOUCH)) {
|
||||||
value += addValue(25, "dt");
|
value += addValue(25, "dt");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Lifelink")) {
|
if (c.hasKeyword(Keyword.LIFELINK)) {
|
||||||
value += addValue(power * 10, "lifelink");
|
value += addValue(power * 10, "lifelink");
|
||||||
}
|
}
|
||||||
if (power > 1 && c.hasKeyword("Trample")) {
|
if (power > 1 && c.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
value += addValue((power - 1) * 5, "trample");
|
value += addValue((power - 1) * 5, "trample");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Vigilance")) {
|
if (c.hasKeyword(Keyword.VIGILANCE)) {
|
||||||
value += addValue((power * 5) + (toughness * 5), "vigilance");
|
value += addValue((power * 5) + (toughness * 5), "vigilance");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Wither")) {
|
if (c.hasKeyword(Keyword.WITHER)) {
|
||||||
value += addValue(power * 10, "Wither");
|
value += addValue(power * 10, "Wither");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Infect")) {
|
if (c.hasKeyword(Keyword.INFECT)) {
|
||||||
value += addValue(power * 15, "infect");
|
value += addValue(power * 15, "infect");
|
||||||
}
|
}
|
||||||
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
|
value += addValue(c.getKeywordMagnitude(Keyword.RAMPAGE), "rampage");
|
||||||
|
value += addValue(c.getKeywordMagnitude(Keyword.AFFLICT) * 5, "afflict");
|
||||||
}
|
}
|
||||||
|
|
||||||
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
|
value += addValue(c.getKeywordMagnitude(Keyword.BUSHIDO) * 16, "bushido");
|
||||||
value += addValue(c.getAmountOfKeyword("Flanking") * 15, "flanking");
|
value += addValue(c.getAmountOfKeyword(Keyword.FLANKING) * 15, "flanking");
|
||||||
value += addValue(c.getAmountOfKeyword("Exalted") * 15, "exalted");
|
value += addValue(c.getAmountOfKeyword(Keyword.EXALTED) * 15, "exalted");
|
||||||
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
|
value += addValue(c.getKeywordMagnitude(Keyword.ANNIHILATOR) * 50, "eldrazi");
|
||||||
value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
|
value += addValue(c.getKeywordMagnitude(Keyword.ABSORB) * 11, "absorb");
|
||||||
|
|
||||||
|
// Keywords that may produce temporary or permanent buffs over time
|
||||||
|
if (c.hasKeyword(Keyword.PROWESS)) {
|
||||||
|
value += addValue(5, "prowess");
|
||||||
|
}
|
||||||
|
if (c.hasKeyword(Keyword.OUTLAST)) {
|
||||||
|
value += addValue(10, "outlast");
|
||||||
|
}
|
||||||
|
|
||||||
// Defensive Keywords
|
// Defensive Keywords
|
||||||
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
|
if (c.hasKeyword(Keyword.REACH) && !c.hasKeyword(Keyword.FLYING)) {
|
||||||
value += addValue(5, "reach");
|
value += addValue(5, "reach");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
|
if (c.hasKeyword("CARDNAME can block creatures with shadow as though they didn't have shadow.")) {
|
||||||
@@ -108,7 +132,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Protection
|
// Protection
|
||||||
if (c.hasKeyword("Indestructible")) {
|
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
value += addValue(70, "darksteel");
|
value += addValue(70, "darksteel");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Prevent all damage that would be dealt to CARDNAME.")) {
|
if (c.hasKeyword("Prevent all damage that would be dealt to CARDNAME.")) {
|
||||||
@@ -116,20 +140,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
} else if (c.hasKeyword("Prevent all combat damage that would be dealt to CARDNAME.")) {
|
} else if (c.hasKeyword("Prevent all combat damage that would be dealt to CARDNAME.")) {
|
||||||
value += addValue(50, "fogbank");
|
value += addValue(50, "fogbank");
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Hexproof")) {
|
if (c.hasKeyword(Keyword.HEXPROOF)) {
|
||||||
value += addValue(35, "hexproof");
|
value += addValue(35, "hexproof");
|
||||||
} else if (c.hasKeyword("Shroud")) {
|
} else if (c.hasKeyword(Keyword.SHROUD)) {
|
||||||
value += addValue(30, "shroud");
|
value += addValue(30, "shroud");
|
||||||
}
|
}
|
||||||
if (c.hasStartOfKeyword("Protection")) {
|
if (c.hasKeyword(Keyword.PROTECTION)) {
|
||||||
value += addValue(20, "protection");
|
value += addValue(20, "protection");
|
||||||
}
|
}
|
||||||
if (c.hasStartOfKeyword("PreventAllDamageBy")) {
|
|
||||||
value += addValue(10, "prevent-dmg");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bad keywords
|
// Bad keywords
|
||||||
if (c.hasKeyword("Defender") || c.hasKeyword("CARDNAME can't attack.")) {
|
if (c.hasKeyword(Keyword.DEFENDER) || c.hasKeyword("CARDNAME can't attack.")) {
|
||||||
value -= subValue((power * 9) + 40, "defender");
|
value -= subValue((power * 9) + 40, "defender");
|
||||||
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
|
} else if (c.getSVar("SacrificeEndCombat").equals("True")) {
|
||||||
value -= subValue(40, "sac-end");
|
value -= subValue(40, "sac-end");
|
||||||
@@ -165,17 +186,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
value -= subValue(30, "cupkeep");
|
value -= subValue(30, "cupkeep");
|
||||||
} else if (c.hasStartOfKeyword("UpkeepCost")) {
|
} else if (c.hasStartOfKeyword("UpkeepCost")) {
|
||||||
value -= subValue(20, "sac-unless");
|
value -= subValue(20, "sac-unless");
|
||||||
} else if (c.hasStartOfKeyword("Echo") && c.cameUnderControlSinceLastUpkeep()) {
|
} else if (c.hasKeyword(Keyword.ECHO) && c.cameUnderControlSinceLastUpkeep()) {
|
||||||
value -= subValue(10, "echo-unpaid");
|
value -= subValue(10, "echo-unpaid");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
|
if (c.hasStartOfKeyword("At the beginning of your upkeep, CARDNAME deals")) {
|
||||||
value -= subValue(20, "upkeep-dmg");
|
value -= subValue(20, "upkeep-dmg");
|
||||||
}
|
}
|
||||||
if (c.hasStartOfKeyword("Fading")) {
|
if (c.hasKeyword(Keyword.FADING)) {
|
||||||
value -= subValue(20, "fading");
|
value -= subValue(20, "fading");
|
||||||
}
|
}
|
||||||
if (c.hasStartOfKeyword("Vanishing")) {
|
if (c.hasKeyword(Keyword.VANISHING)) {
|
||||||
value -= subValue(20, "vanishing");
|
value -= subValue(20, "vanishing");
|
||||||
}
|
}
|
||||||
if (c.getSVar("Targeting").equals("Dies")) {
|
if (c.getSVar("Targeting").equals("Dies")) {
|
||||||
@@ -184,7 +205,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 +224,44 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
|||||||
if (!c.getEncodedCards().isEmpty()) {
|
if (!c.getEncodedCards().isEmpty()) {
|
||||||
value += addValue(24, "encoded");
|
value += addValue(24, "encoded");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// card-specific evaluation modifier
|
||||||
|
if (c.hasSVar("AIEvaluationModifier")) {
|
||||||
|
int mod = AbilityUtils.calculateAmount(c, c.getSVar("AIEvaluationModifier"), null);
|
||||||
|
value += mod;
|
||||||
|
}
|
||||||
|
|
||||||
return value;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,12 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.io.BufferedReader;
|
|
||||||
import java.io.InputStream;
|
|
||||||
import java.io.InputStreamReader;
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.EnumMap;
|
|
||||||
import java.util.HashMap;
|
|
||||||
import java.util.HashSet;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Map.Entry;
|
|
||||||
import java.util.Set;
|
|
||||||
|
|
||||||
import com.google.common.collect.ArrayListMultimap;
|
import com.google.common.collect.ArrayListMultimap;
|
||||||
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 com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
|
|
||||||
import forge.StaticData;
|
import forge.StaticData;
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
|
import forge.card.MagicColor;
|
||||||
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;
|
||||||
@@ -27,14 +14,16 @@ import forge.game.ability.effects.DetachedCardEffect;
|
|||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardFactory;
|
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
|
import forge.game.card.token.TokenInfo;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
import forge.game.event.GameEventAttackersDeclared;
|
import forge.game.event.GameEventAttackersDeclared;
|
||||||
import forge.game.event.GameEventCombatChanged;
|
import forge.game.event.GameEventCombatChanged;
|
||||||
|
import forge.game.mana.ManaPool;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.AbilityManaPart;
|
||||||
import forge.game.spellability.SpellAbility;
|
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;
|
||||||
@@ -45,6 +34,12 @@ import forge.util.TextUtil;
|
|||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.io.BufferedReader;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.Map.Entry;
|
||||||
|
|
||||||
public abstract class GameState {
|
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>();
|
||||||
static {
|
static {
|
||||||
@@ -60,16 +55,29 @@ public abstract class GameState {
|
|||||||
private int computerLife = -1;
|
private int computerLife = -1;
|
||||||
private String humanCounters = "";
|
private String humanCounters = "";
|
||||||
private String computerCounters = "";
|
private String computerCounters = "";
|
||||||
|
private String humanManaPool = "";
|
||||||
|
private String computerManaPool = "";
|
||||||
|
private String humanPersistentMana = "";
|
||||||
|
private String computerPersistentMana = "";
|
||||||
|
private int humanLandsPlayed = 0;
|
||||||
|
private int computerLandsPlayed = 0;
|
||||||
|
private int humanLandsPlayedLastTurn = 0;
|
||||||
|
private int computerLandsPlayedLastTurn = 0;
|
||||||
|
|
||||||
|
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> cardToEnchantPlayerId = new HashMap<>();
|
||||||
private final Map<Card, Integer> markedDamage = new HashMap<>();
|
private final Map<Card, Integer> markedDamage = new HashMap<>();
|
||||||
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
|
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
|
||||||
private final Map<Card, String> cardToChosenType = 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>> cardToRememberedId = new HashMap<>();
|
||||||
|
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
|
||||||
|
private final Map<Card, String> cardToNamedCard = new HashMap<>();
|
||||||
private final Map<Card, String> cardToExiledWithId = new HashMap<>();
|
private final Map<Card, String> cardToExiledWithId = new HashMap<>();
|
||||||
private final Map<Card, Card> cardAttackMap = new HashMap<>();
|
private final Map<Card, Card> cardAttackMap = new HashMap<>();
|
||||||
|
|
||||||
@@ -78,13 +86,18 @@ public abstract class GameState {
|
|||||||
private final Map<String, String> abilityString = new HashMap<>();
|
private final Map<String, String> abilityString = new HashMap<>();
|
||||||
|
|
||||||
private final Set<Card> cardsReferencedByID = new HashSet<>();
|
private final Set<Card> cardsReferencedByID = new HashSet<>();
|
||||||
|
private final Set<Card> cardsWithoutETBTrigs = new HashSet<>();
|
||||||
|
|
||||||
private String tChangePlayer = "NONE";
|
private String tChangePlayer = "NONE";
|
||||||
private String tChangePhase = "NONE";
|
private String tChangePhase = "NONE";
|
||||||
|
|
||||||
|
private String tAdvancePhase = "NONE";
|
||||||
|
|
||||||
private String precastHuman = null;
|
private String precastHuman = null;
|
||||||
private String precastAI = null;
|
private String precastAI = null;
|
||||||
|
|
||||||
|
private int turn = 1;
|
||||||
|
|
||||||
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
|
// 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_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
|
||||||
private final int TARGET_HUMAN = -2;
|
private final int TARGET_HUMAN = -2;
|
||||||
@@ -98,18 +111,43 @@ 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"));
|
||||||
|
sb.append(TextUtil.concatNoSpace("humanlandsplayed=", String.valueOf(humanLandsPlayed), "\n"));
|
||||||
|
sb.append(TextUtil.concatNoSpace("ailandsplayed=", String.valueOf(computerLandsPlayed), "\n"));
|
||||||
|
sb.append(TextUtil.concatNoSpace("humanlandsplayedlastturn=", String.valueOf(humanLandsPlayedLastTurn), "\n"));
|
||||||
|
sb.append(TextUtil.concatNoSpace("ailandsplayedlastturn=", String.valueOf(computerLandsPlayedLastTurn), "\n"));
|
||||||
|
sb.append(TextUtil.concatNoSpace("turn=", String.valueOf(turn), "\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));
|
if (!humanManaPool.isEmpty()) {
|
||||||
sb.append(String.format("activephase=%s\n", tChangePhase));
|
sb.append(TextUtil.concatNoSpace("humanmanapool=", humanManaPool, "\n"));
|
||||||
|
}
|
||||||
|
if (!computerManaPool.isEmpty()) {
|
||||||
|
sb.append(TextUtil.concatNoSpace("aimanapool=", humanManaPool, "\n"));
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
|
||||||
|
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();
|
||||||
@@ -117,13 +155,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");
|
||||||
}
|
}
|
||||||
@@ -134,11 +172,18 @@ public abstract class GameState {
|
|||||||
}
|
}
|
||||||
humanLife = human.getLife();
|
humanLife = human.getLife();
|
||||||
computerLife = ai.getLife();
|
computerLife = ai.getLife();
|
||||||
|
humanLandsPlayed = human.getLandsPlayedThisTurn();
|
||||||
|
computerLandsPlayed = ai.getLandsPlayedThisTurn();
|
||||||
|
humanLandsPlayedLastTurn = human.getLandsPlayedLastTurn();
|
||||||
|
computerLandsPlayedLastTurn = ai.getLandsPlayedLastTurn();
|
||||||
humanCounters = countersToString(human.getCounters());
|
humanCounters = countersToString(human.getCounters());
|
||||||
computerCounters = countersToString(ai.getCounters());
|
computerCounters = countersToString(ai.getCounters());
|
||||||
|
humanManaPool = processManaPool(human.getManaPool());
|
||||||
|
computerManaPool = processManaPool(ai.getManaPool());
|
||||||
|
|
||||||
tChangePlayer = game.getPhaseHandler().getPlayerTurn() == ai ? "ai" : "human";
|
tChangePlayer = game.getPhaseHandler().getPlayerTurn() == ai ? "ai" : "human";
|
||||||
tChangePhase = game.getPhaseHandler().getPhase().toString();
|
tChangePhase = game.getPhaseHandler().getPhase().toString();
|
||||||
|
turn = game.getPhaseHandler().getTurn();
|
||||||
aiCardTexts.clear();
|
aiCardTexts.clear();
|
||||||
humanCardTexts.clear();
|
humanCardTexts.clear();
|
||||||
|
|
||||||
@@ -160,11 +205,14 @@ public abstract class GameState {
|
|||||||
}
|
}
|
||||||
for (Object o : card.getRemembered()) {
|
for (Object o : card.getRemembered()) {
|
||||||
// Remember the IDs of remembered cards
|
// Remember the IDs of remembered cards
|
||||||
// TODO: we can currently support remembered cards only. Expand to support other remembered objects.
|
|
||||||
if (o instanceof Card) {
|
if (o instanceof Card) {
|
||||||
cardsReferencedByID.add((Card)o);
|
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)) {
|
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
|
||||||
// Remember the IDs of attacked planeswalkers
|
// Remember the IDs of attacked planeswalkers
|
||||||
GameEntity def = game.getCombat().getDefenderByAttacker(card);
|
GameEntity def = game.getCombat().getDefenderByAttacker(card);
|
||||||
@@ -181,6 +229,9 @@ public abstract class GameState {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
@@ -195,7 +246,7 @@ public abstract class GameState {
|
|||||||
newText.append(";");
|
newText.append(";");
|
||||||
}
|
}
|
||||||
if (c.isToken()) {
|
if (c.isToken()) {
|
||||||
newText.append("t:" + new CardFactory.TokenInfo(c).toString());
|
newText.append("t:" + new TokenInfo(c).toString());
|
||||||
} else {
|
} else {
|
||||||
if (c.getPaperCard() == null) {
|
if (c.getPaperCard() == null) {
|
||||||
return;
|
return;
|
||||||
@@ -221,8 +272,7 @@ public abstract class GameState {
|
|||||||
newText.append("|Renowned");
|
newText.append("|Renowned");
|
||||||
}
|
}
|
||||||
if (c.isMonstrous()) {
|
if (c.isMonstrous()) {
|
||||||
newText.append("|Monstrous:");
|
newText.append("|Monstrous");
|
||||||
newText.append(c.getMonstrosityNum());
|
|
||||||
}
|
}
|
||||||
if (c.isPhasedOut()) {
|
if (c.isPhasedOut()) {
|
||||||
newText.append("|PhasedOut");
|
newText.append("|PhasedOut");
|
||||||
@@ -240,11 +290,6 @@ public abstract class GameState {
|
|||||||
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
|
||||||
newText.append("|Meld");
|
newText.append("|Meld");
|
||||||
}
|
}
|
||||||
Map<CounterType, Integer> counters = c.getCounters();
|
|
||||||
if (!counters.isEmpty()) {
|
|
||||||
newText.append("|Counters:");
|
|
||||||
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());
|
||||||
} else if (c.getFortifying() != null) {
|
} else if (c.getFortifying() != null) {
|
||||||
@@ -252,6 +297,12 @@ public abstract class GameState {
|
|||||||
} else if (c.getEnchantingCard() != null) {
|
} else if (c.getEnchantingCard() != null) {
|
||||||
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
|
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
|
||||||
}
|
}
|
||||||
|
if (c.getEnchantingPlayer() != null) {
|
||||||
|
// TODO: improve this for game states with more than two players
|
||||||
|
newText.append("|EnchantingPlayer:");
|
||||||
|
Player p = c.getEnchantingPlayer();
|
||||||
|
newText.append(p.getController().isAI() ? "AI" : "HUMAN");
|
||||||
|
}
|
||||||
|
|
||||||
if (c.getDamage() > 0) {
|
if (c.getDamage() > 0) {
|
||||||
newText.append("|Damage:").append(c.getDamage());
|
newText.append("|Damage:").append(c.getDamage());
|
||||||
@@ -263,6 +314,9 @@ public abstract class GameState {
|
|||||||
if (!c.getChosenType().isEmpty()) {
|
if (!c.getChosenType().isEmpty()) {
|
||||||
newText.append("|ChosenType:").append(c.getChosenType());
|
newText.append("|ChosenType:").append(c.getChosenType());
|
||||||
}
|
}
|
||||||
|
if (!c.getNamedCard().isEmpty()) {
|
||||||
|
newText.append("|NamedCard:").append(c.getNamedCard());
|
||||||
|
}
|
||||||
|
|
||||||
List<String> rememberedCardIds = Lists.newArrayList();
|
List<String> rememberedCardIds = Lists.newArrayList();
|
||||||
for (Object obj : c.getRemembered()) {
|
for (Object obj : c.getRemembered()) {
|
||||||
@@ -274,12 +328,33 @@ public abstract class GameState {
|
|||||||
if (!rememberedCardIds.isEmpty()) {
|
if (!rememberedCardIds.isEmpty()) {
|
||||||
newText.append("|RememberedCards:").append(TextUtil.join(rememberedCardIds, ","));
|
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 (zoneType == ZoneType.Exile) {
|
||||||
if (c.getExiledWith() != null) {
|
if (c.getExiledWith() != null) {
|
||||||
newText.append("|ExiledWith:").append(c.getExiledWith().getId());
|
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() != null) {
|
||||||
@@ -305,7 +380,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();
|
||||||
}
|
}
|
||||||
@@ -350,14 +425,20 @@ public abstract class GameState {
|
|||||||
if (categoryName.startsWith("active")) {
|
if (categoryName.startsWith("active")) {
|
||||||
if (categoryName.endsWith("player"))
|
if (categoryName.endsWith("player"))
|
||||||
tChangePlayer = categoryValue.trim().toLowerCase();
|
tChangePlayer = categoryValue.trim().toLowerCase();
|
||||||
if (categoryName.endsWith("phase"))
|
else if (categoryName.endsWith("phase"))
|
||||||
tChangePhase = categoryValue.trim().toUpperCase();
|
tChangePhase = categoryValue.trim().toUpperCase();
|
||||||
|
else if (categoryName.endsWith("phaseadvance"))
|
||||||
|
tAdvancePhase = categoryValue.trim().toUpperCase();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
boolean isHuman = categoryName.startsWith("human");
|
boolean isHuman = categoryName.startsWith("human");
|
||||||
|
|
||||||
if (categoryName.endsWith("life")) {
|
if (categoryName.equals("turn")) {
|
||||||
|
turn = Integer.parseInt(categoryValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (categoryName.endsWith("life")) {
|
||||||
if (isHuman)
|
if (isHuman)
|
||||||
humanLife = Integer.parseInt(categoryValue);
|
humanLife = Integer.parseInt(categoryValue);
|
||||||
else
|
else
|
||||||
@@ -371,6 +452,20 @@ public abstract class GameState {
|
|||||||
computerCounters = categoryValue;
|
computerCounters = categoryValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (categoryName.endsWith("landsplayed")) {
|
||||||
|
if (isHuman)
|
||||||
|
humanLandsPlayed = Integer.parseInt(categoryValue);
|
||||||
|
else
|
||||||
|
computerLandsPlayed = Integer.parseInt(categoryValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (categoryName.endsWith("landsplayedlastturn")) {
|
||||||
|
if (isHuman)
|
||||||
|
humanLandsPlayedLastTurn = Integer.parseInt(categoryValue);
|
||||||
|
else
|
||||||
|
computerLandsPlayedLastTurn = Integer.parseInt(categoryValue);
|
||||||
|
}
|
||||||
|
|
||||||
else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) {
|
else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) {
|
||||||
if (isHuman)
|
if (isHuman)
|
||||||
humanCardTexts.put(ZoneType.Battlefield, categoryValue);
|
humanCardTexts.put(ZoneType.Battlefield, categoryValue);
|
||||||
@@ -423,6 +518,21 @@ public abstract class GameState {
|
|||||||
else
|
else
|
||||||
precastAI = categoryValue;
|
precastAI = categoryValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else if (categoryName.endsWith("manapool")) {
|
||||||
|
if (isHuman)
|
||||||
|
humanManaPool = categoryValue;
|
||||||
|
else
|
||||||
|
computerManaPool = categoryValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (categoryName.endsWith("persistentmana")) {
|
||||||
|
if (isHuman)
|
||||||
|
humanPersistentMana = categoryValue;
|
||||||
|
else
|
||||||
|
computerPersistentMana = categoryValue;
|
||||||
|
}
|
||||||
|
|
||||||
else {
|
else {
|
||||||
System.out.println("Unknown key: " + categoryName);
|
System.out.println("Unknown key: " + categoryName);
|
||||||
}
|
}
|
||||||
@@ -443,6 +553,7 @@ public abstract class GameState {
|
|||||||
|
|
||||||
idToCard.clear();
|
idToCard.clear();
|
||||||
cardToAttachId.clear();
|
cardToAttachId.clear();
|
||||||
|
cardToEnchantPlayerId.clear();
|
||||||
cardToRememberedId.clear();
|
cardToRememberedId.clear();
|
||||||
cardToExiledWithId.clear();
|
cardToExiledWithId.clear();
|
||||||
markedDamage.clear();
|
markedDamage.clear();
|
||||||
@@ -451,12 +562,18 @@ public abstract class GameState {
|
|||||||
cardToScript.clear();
|
cardToScript.clear();
|
||||||
cardAttackMap.clear();
|
cardAttackMap.clear();
|
||||||
|
|
||||||
Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
|
Player newPlayerTurn = tChangePlayer.equalsIgnoreCase("human") ? human : tChangePlayer.equalsIgnoreCase("ai") ? ai : null;
|
||||||
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
|
PhaseType newPhase = tChangePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tChangePhase);
|
||||||
|
PhaseType advPhase = tAdvancePhase.equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tAdvancePhase);
|
||||||
|
|
||||||
// Set stack to resolving so things won't trigger/effects be checked right away
|
// Set stack to resolving so things won't trigger/effects be checked right away
|
||||||
game.getStack().setResolving(true);
|
game.getStack().setResolving(true);
|
||||||
|
|
||||||
|
updateManaPool(human, humanManaPool, true, false);
|
||||||
|
updateManaPool(ai, computerManaPool, true, false);
|
||||||
|
updateManaPool(human, humanPersistentMana, false, true);
|
||||||
|
updateManaPool(ai, computerPersistentMana, false, true);
|
||||||
|
|
||||||
if (!humanCounters.isEmpty()) {
|
if (!humanCounters.isEmpty()) {
|
||||||
applyCountersToGameEntity(human, humanCounters);
|
applyCountersToGameEntity(human, humanCounters);
|
||||||
}
|
}
|
||||||
@@ -464,12 +581,12 @@ public abstract class GameState {
|
|||||||
applyCountersToGameEntity(ai, computerCounters);
|
applyCountersToGameEntity(ai, computerCounters);
|
||||||
}
|
}
|
||||||
|
|
||||||
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn);
|
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn, turn);
|
||||||
|
|
||||||
game.getTriggerHandler().setSuppressAllTriggers(true);
|
game.getTriggerHandler().setSuppressAllTriggers(true);
|
||||||
|
|
||||||
setupPlayerState(humanLife, humanCardTexts, human);
|
setupPlayerState(humanLife, humanCardTexts, human, humanLandsPlayed, humanLandsPlayedLastTurn);
|
||||||
setupPlayerState(computerLife, aiCardTexts, ai);
|
setupPlayerState(computerLife, aiCardTexts, ai, computerLandsPlayed, computerLandsPlayedLastTurn);
|
||||||
|
|
||||||
handleCardAttachments();
|
handleCardAttachments();
|
||||||
handleChosenEntities();
|
handleChosenEntities();
|
||||||
@@ -489,13 +606,54 @@ public abstract class GameState {
|
|||||||
|
|
||||||
game.getStack().setResolving(false);
|
game.getStack().setResolving(false);
|
||||||
|
|
||||||
|
// Advance to a certain phase, activating all triggered abilities
|
||||||
|
if (advPhase != null) {
|
||||||
|
game.getPhaseHandler().devAdvanceToPhase(advPhase);
|
||||||
|
}
|
||||||
|
|
||||||
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 String processManaPool(ManaPool manaPool) {
|
||||||
|
String mana = "";
|
||||||
|
for (final byte c : MagicColor.WUBRGC) {
|
||||||
|
int amount = manaPool.getAmountOfColor(c);
|
||||||
|
for (int i = 0; i < amount; i++) {
|
||||||
|
mana += MagicColor.toShortString(c) + " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mana.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateManaPool(Player p, String manaDef, boolean clearPool, boolean persistent) {
|
||||||
|
Game game = p.getGame();
|
||||||
|
if (clearPool) {
|
||||||
|
p.getManaPool().clearPool(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!manaDef.isEmpty()) {
|
||||||
|
final Card dummy = new Card(-777777, game);
|
||||||
|
dummy.setOwner(p);
|
||||||
|
final Map<String, String> produced = Maps.newHashMap();
|
||||||
|
produced.put("Produced", manaDef);
|
||||||
|
if (persistent) {
|
||||||
|
produced.put("PersistentMana", "True");
|
||||||
|
}
|
||||||
|
final AbilityManaPart abMana = new AbilityManaPart(dummy, produced);
|
||||||
|
game.getAction().invoke(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
abMana.produceMana(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void handleCombat(final Game game, final Player attackingPlayer, final Player defendingPlayer, final boolean toDeclareBlockers) {
|
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,
|
// First we need to ensure that all attackers are declared in the Declare Attackers step,
|
||||||
// even if proceeding straight to Declare Blockers
|
// even if proceeding straight to Declare Blockers
|
||||||
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, attackingPlayer);
|
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, attackingPlayer, turn);
|
||||||
|
|
||||||
if (game.getPhaseHandler().getCombat() == null) {
|
if (game.getPhaseHandler().getCombat() == null) {
|
||||||
game.getPhaseHandler().setCombat(new Combat(attackingPlayer));
|
game.getPhaseHandler().setCombat(new Combat(attackingPlayer));
|
||||||
@@ -558,6 +716,17 @@ public abstract class GameState {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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
|
// Exiled with X
|
||||||
for (Entry<Card, String> rememberedEnts : cardToExiledWithId.entrySet()) {
|
for (Entry<Card, String> rememberedEnts : cardToExiledWithId.entrySet()) {
|
||||||
Card c = rememberedEnts.getKey();
|
Card c = rememberedEnts.getKey();
|
||||||
@@ -613,10 +782,10 @@ public abstract class GameState {
|
|||||||
private void executeScript(Game game, Card c, String sPtr) {
|
private void executeScript(Game game, Card c, String sPtr) {
|
||||||
int tgtID = TARGET_NONE;
|
int tgtID = TARGET_NONE;
|
||||||
if (sPtr.contains("->")) {
|
if (sPtr.contains("->")) {
|
||||||
String tgtDef = sPtr.substring(sPtr.indexOf("->") + 2);
|
String tgtDef = sPtr.substring(sPtr.lastIndexOf("->") + 2);
|
||||||
|
|
||||||
tgtID = parseTargetInScript(tgtDef);
|
tgtID = parseTargetInScript(tgtDef);
|
||||||
sPtr = sPtr.substring(0, sPtr.indexOf("->"));
|
sPtr = sPtr.substring(0, sPtr.lastIndexOf("->"));
|
||||||
}
|
}
|
||||||
|
|
||||||
SpellAbility sa = null;
|
SpellAbility sa = null;
|
||||||
@@ -628,22 +797,72 @@ public abstract class GameState {
|
|||||||
System.err.println("ERROR: Unable to find SA with index " + numSA + " on card " + c + " to execute!");
|
System.err.println("ERROR: Unable to find SA with index " + numSA + " on card " + c + " to execute!");
|
||||||
}
|
}
|
||||||
} else {
|
} 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)) {
|
if (!c.hasSVar(sPtr)) {
|
||||||
System.err.println("ERROR: Unable to find SVar " + sPtr + " on card " + c + " + to execute!");
|
System.err.println("ERROR: Unable to find SVar " + sPtr + " on card " + c + " + to execute!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String svarValue = c.getSVar(sPtr);
|
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);
|
sa = AbilityFactory.getAbility(svarValue, c);
|
||||||
if (sa == null) {
|
if (sa == null) {
|
||||||
System.err.println("ERROR: Unable to generate ability for SVar " + svarValue);
|
System.err.println("ERROR: Unable to generate ability for SVar " + svarValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sa.setActivatingPlayer(c.getController());
|
sa.setActivatingPlayer(c.getController());
|
||||||
handleScriptedTargetingForSA(game, sa, tgtID);
|
handleScriptedTargetingForSA(game, sa, tgtID);
|
||||||
|
|
||||||
sa.resolve();
|
sa.resolve();
|
||||||
|
|
||||||
|
// resolve subabilities
|
||||||
|
SpellAbility subSa = sa.getSubAbility();
|
||||||
|
while (subSa != null) {
|
||||||
|
subSa.resolve();
|
||||||
|
subSa = subSa.getSubAbility();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handlePrecastSpells(final Game game) {
|
private void handlePrecastSpells(final Game game) {
|
||||||
@@ -670,12 +889,12 @@ public abstract class GameState {
|
|||||||
|
|
||||||
if (spellDef.contains(":")) {
|
if (spellDef.contains(":")) {
|
||||||
// targeting via -> will be handled in executeScript
|
// targeting via -> will be handled in executeScript
|
||||||
scriptID = spellDef.substring(spellDef.indexOf(":") + 1);
|
scriptID = spellDef.substring(spellDef.indexOf(":") + 1).trim();
|
||||||
spellDef = spellDef.substring(0, spellDef.indexOf(":"));
|
spellDef = spellDef.substring(0, spellDef.indexOf(":")).trim();
|
||||||
} else if (spellDef.contains("->")) {
|
} else if (spellDef.contains("->")) {
|
||||||
String tgtDef = spellDef.substring(spellDef.indexOf("->") + 2);
|
String tgtDef = spellDef.substring(spellDef.indexOf("->") + 2).trim();
|
||||||
tgtID = parseTargetInScript(tgtDef);
|
tgtID = parseTargetInScript(tgtDef);
|
||||||
spellDef = spellDef.substring(0, spellDef.indexOf("->"));
|
spellDef = spellDef.substring(0, spellDef.indexOf("->")).trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
|
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
|
||||||
@@ -728,6 +947,12 @@ public abstract class GameState {
|
|||||||
Card c = entry.getKey();
|
Card c = entry.getKey();
|
||||||
c.setChosenType(entry.getValue());
|
c.setChosenType(entry.getValue());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Named card
|
||||||
|
for (Entry<Card, String> entry : cardToNamedCard.entrySet()) {
|
||||||
|
Card c = entry.getKey();
|
||||||
|
c.setNamedCard(entry.getValue());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCardAttachments() {
|
private void handleCardAttachments() {
|
||||||
@@ -755,10 +980,20 @@ public abstract class GameState {
|
|||||||
attacher.fortifyCard(attachedTo);
|
attacher.fortifyCard(attachedTo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Enchant players by ID
|
||||||
|
for(Entry<Card, Integer> entry : cardToEnchantPlayerId.entrySet()) {
|
||||||
|
// TODO: improve this for game states with more than two players
|
||||||
|
Card attacher = entry.getKey();
|
||||||
|
Game game = attacher.getGame();
|
||||||
|
Player attachedTo = entry.getValue() == TARGET_AI ? game.getPlayers().get(1) : game.getPlayers().get(0);
|
||||||
|
|
||||||
|
attacher.enchantEntity(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);
|
||||||
@@ -766,7 +1001,7 @@ 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, final int landsPlayed, final int landsPlayedLastTurn) {
|
||||||
// Lock check static as we setup player state
|
// Lock check static as we setup player state
|
||||||
|
|
||||||
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
|
||||||
@@ -776,6 +1011,9 @@ public abstract class GameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (life >= 0) p.setLife(life, null);
|
if (life >= 0) p.setLife(life, null);
|
||||||
|
p.setLandsPlayedThisTurn(landsPlayed);
|
||||||
|
p.setLandsPlayedLastTurn(landsPlayedLastTurn);
|
||||||
|
|
||||||
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
|
for (Entry<ZoneType, CardCollectionView> kv : playerCards.entrySet()) {
|
||||||
PlayerZone zone = p.getZone(kv.getKey());
|
PlayerZone zone = p.getZone(kv.getKey());
|
||||||
if (kv.getKey() == ZoneType.Battlefield) {
|
if (kv.getKey() == ZoneType.Battlefield) {
|
||||||
@@ -796,14 +1034,16 @@ public abstract class GameState {
|
|||||||
// 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(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
|
c.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
|
||||||
p.getZone(ZoneType.Hand).add(c);
|
|
||||||
if (c.isAura()) {
|
if (c.isAura()) {
|
||||||
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
|
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
|
||||||
// (will be overridden later, so the actual value shouldn't matter)
|
// (will be overridden later, so the actual value shouldn't matter)
|
||||||
c.setEnchanting(c);
|
c.setEnchanting(c);
|
||||||
|
}
|
||||||
|
|
||||||
p.getGame().getAction().moveToPlay(c, null);
|
if (cardsWithoutETBTrigs.contains(c)) {
|
||||||
|
p.getGame().getAction().moveTo(ZoneType.Battlefield, c, null);
|
||||||
} else {
|
} else {
|
||||||
|
p.getZone(ZoneType.Hand).add(c);
|
||||||
p.getGame().getAction().moveToPlay(c, null);
|
p.getGame().getAction().moveToPlay(c, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,8 +1085,9 @@ public abstract class GameState {
|
|||||||
Card c;
|
Card c;
|
||||||
boolean hasSetCurSet = false;
|
boolean hasSetCurSet = false;
|
||||||
if (cardinfo[0].startsWith("t:")) {
|
if (cardinfo[0].startsWith("t:")) {
|
||||||
|
// TODO Make sure Game State conversion works with new tokens
|
||||||
String tokenStr = cardinfo[0].substring(2);
|
String tokenStr = cardinfo[0].substring(2);
|
||||||
c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player);
|
c = new TokenInfo(tokenStr).makeOneToken(player);
|
||||||
} else {
|
} else {
|
||||||
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode);
|
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode);
|
||||||
if (pc == null) {
|
if (pc == null) {
|
||||||
@@ -866,9 +1107,8 @@ public abstract class GameState {
|
|||||||
c.tap();
|
c.tap();
|
||||||
} else if (info.startsWith("Renowned")) {
|
} else if (info.startsWith("Renowned")) {
|
||||||
c.setRenowned(true);
|
c.setRenowned(true);
|
||||||
} else if (info.startsWith("Monstrous:")) {
|
} else if (info.startsWith("Monstrous")) {
|
||||||
c.setMonstrous(true);
|
c.setMonstrous(true);
|
||||||
c.setMonstrosityNum(Integer.parseInt(info.substring((info.indexOf(':') + 1))));
|
|
||||||
} else if (info.startsWith("PhasedOut")) {
|
} else if (info.startsWith("PhasedOut")) {
|
||||||
c.setPhasedOut(true);
|
c.setPhasedOut(true);
|
||||||
} else if (info.startsWith("Counters:")) {
|
} else if (info.startsWith("Counters:")) {
|
||||||
@@ -897,6 +1137,10 @@ public abstract class GameState {
|
|||||||
} else if (info.startsWith("Attaching:")) {
|
} else if (info.startsWith("Attaching:")) {
|
||||||
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
|
||||||
cardToAttachId.put(c, id);
|
cardToAttachId.put(c, id);
|
||||||
|
} else if (info.startsWith("EnchantingPlayer:")) {
|
||||||
|
// TODO: improve this for game states with more than two players
|
||||||
|
String tgt = info.substring(info.indexOf(':') + 1);
|
||||||
|
cardToEnchantPlayerId.put(c, tgt.equalsIgnoreCase("AI") ? TARGET_AI : TARGET_HUMAN);
|
||||||
} 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));
|
||||||
@@ -907,10 +1151,14 @@ public abstract class GameState {
|
|||||||
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||||
} else if (info.startsWith("ChosenType:")) {
|
} else if (info.startsWith("ChosenType:")) {
|
||||||
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
|
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
|
||||||
|
} else if (info.startsWith("NamedCard:")) {
|
||||||
|
cardToNamedCard.put(c, info.substring(info.indexOf(':') + 1));
|
||||||
} else if (info.startsWith("ExecuteScript:")) {
|
} else if (info.startsWith("ExecuteScript:")) {
|
||||||
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
|
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
|
||||||
} else if (info.startsWith("RememberedCards:")) {
|
} else if (info.startsWith("RememberedCards:")) {
|
||||||
cardToRememberedId.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
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:")) {
|
} else if (info.startsWith("ExiledWith:")) {
|
||||||
cardToExiledWithId.put(c, info.substring(info.indexOf(':') + 1));
|
cardToExiledWithId.put(c, info.substring(info.indexOf(':') + 1));
|
||||||
} else if (info.startsWith("Attacking")) {
|
} else if (info.startsWith("Attacking")) {
|
||||||
@@ -920,6 +1168,8 @@ public abstract class GameState {
|
|||||||
} else {
|
} else {
|
||||||
cardAttackMap.put(c, null);
|
cardAttackMap.put(c, null);
|
||||||
}
|
}
|
||||||
|
} else if (info.equals("NoETBTrigs")) {
|
||||||
|
cardsWithoutETBTrigs.add(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,23 +1,14 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.security.InvalidParameterException;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
import forge.card.CardStateName;
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
|
||||||
import org.apache.commons.lang3.tuple.Pair;
|
|
||||||
|
|
||||||
import com.esotericsoftware.minlog.Log;
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.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.ListMultimap;
|
import com.google.common.collect.ListMultimap;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
import com.google.common.collect.Multimap;
|
import com.google.common.collect.Multimap;
|
||||||
|
|
||||||
import forge.LobbyPlayer;
|
import forge.LobbyPlayer;
|
||||||
import forge.ai.ability.ProtectAi;
|
import forge.ai.ability.ProtectAi;
|
||||||
|
import forge.card.CardStateName;
|
||||||
import forge.card.ColorSet;
|
import forge.card.ColorSet;
|
||||||
import forge.card.ICardFace;
|
import forge.card.ICardFace;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
@@ -33,18 +24,12 @@ import forge.game.ability.ApiType;
|
|||||||
import forge.game.card.*;
|
import forge.game.card.*;
|
||||||
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.*;
|
||||||
import forge.game.cost.CostPart;
|
|
||||||
import forge.game.cost.CostPartMana;
|
|
||||||
import forge.game.mana.Mana;
|
import forge.game.mana.Mana;
|
||||||
import forge.game.mana.ManaCostBeingPaid;
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.player.DelayedReveal;
|
import forge.game.player.*;
|
||||||
import forge.game.player.Player;
|
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
|
||||||
import forge.game.player.PlayerController;
|
|
||||||
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.WrappedAbility;
|
import forge.game.trigger.WrappedAbility;
|
||||||
@@ -55,6 +40,12 @@ import forge.util.ITriggerEvent;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
import forge.util.collect.FCollectionView;
|
import forge.util.collect.FCollectionView;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||||
|
import org.apache.commons.lang3.tuple.Pair;
|
||||||
|
|
||||||
|
import java.security.InvalidParameterException;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,12 +160,21 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title) {
|
public <T extends GameEntity> List<T> chooseEntitiesForEffect(
|
||||||
|
FCollectionView<T> optionList, DelayedReveal delayedReveal, SpellAbility sa, String title,
|
||||||
|
Player targetedPlayer) {
|
||||||
|
// this isn't used
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public SpellAbility chooseSingleSpellForEffect(java.util.List<SpellAbility> spells, SpellAbility sa, String title,
|
||||||
|
Map<String, Object> params) {
|
||||||
ApiType api = sa.getApi();
|
ApiType api = sa.getApi();
|
||||||
if (null == api) {
|
if (null == api) {
|
||||||
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
throw new InvalidParameterException("SA is not api-based, this is not supported yet");
|
||||||
}
|
}
|
||||||
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells);
|
return SpellApiToAi.Converter.get(api).chooseSingleSpellAbility(player, sa, spells, params);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -288,7 +288,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// put the rest on top in random order
|
// put the rest on top in random order
|
||||||
Collections.shuffle(toTop);
|
Collections.shuffle(toTop, MyRandom.getRandom());
|
||||||
return ImmutablePair.of(toTop, toBottom);
|
return ImmutablePair.of(toTop, toBottom);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,8 +298,43 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone) {
|
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
|
||||||
//TODO Add logic for AI ordering here
|
//TODO Add more logic for AI ordering here
|
||||||
|
|
||||||
|
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
|
||||||
|
if (destinationZone == ZoneType.Graveyard) {
|
||||||
|
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
|
||||||
|
return card.getName().equals("Volrath's Shapeshifter")
|
||||||
|
|| card.getStates().contains(CardStateName.OriginalText) && card.getState(CardStateName.OriginalText).getName().equals("Volrath's Shapeshifter");
|
||||||
|
}
|
||||||
|
}).isEmpty()) {
|
||||||
|
int bestValue = 0;
|
||||||
|
Card bestCreature = null;
|
||||||
|
for (Card c : cards) {
|
||||||
|
int curValue = ComputerUtilCard.evaluateCreature(c);
|
||||||
|
if (c.isCreature() && curValue > bestValue) {
|
||||||
|
bestValue = curValue;
|
||||||
|
bestCreature = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestCreature != null) {
|
||||||
|
CardCollection reordered = new CardCollection();
|
||||||
|
for (Card c : cards) {
|
||||||
|
if (!c.equals(bestCreature)) {
|
||||||
|
reordered.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reordered.add(bestCreature);
|
||||||
|
return reordered;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: return with the same order as was passed into this method
|
||||||
return cards;
|
return cards;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,7 +406,7 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty())
|
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty())
|
||||||
{
|
{
|
||||||
chosen = validTypes.get(0);
|
chosen = validTypes.get(0);
|
||||||
Log.warn("AI has no idea how to choose " + kindOfType +", defaulting to 1st element: chosen");
|
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to 1st element: chosen");
|
||||||
}
|
}
|
||||||
game.getAction().nofityOfValue(sa, player, chosen, player);
|
game.getAction().nofityOfValue(sa, player, chosen, player);
|
||||||
return chosen;
|
return chosen;
|
||||||
@@ -414,8 +449,10 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
@Override
|
@Override
|
||||||
public void playChosenSpellAbility(SpellAbility sa) {
|
public void playChosenSpellAbility(SpellAbility sa) {
|
||||||
// System.out.println("Playing sa: " + sa);
|
// System.out.println("Playing sa: " + sa);
|
||||||
if (sa == sa.getHostCard().getGame().PLAY_LAND_SURROGATE) {
|
if (sa instanceof LandAbility) {
|
||||||
player.playLand(sa.getHostCard(), false);
|
if (sa.canPlay()) {
|
||||||
|
sa.resolve();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ComputerUtil.handlePlayingSpellAbility(player, sa, game);
|
ComputerUtil.handlePlayingSpellAbility(player, sa, game);
|
||||||
}
|
}
|
||||||
@@ -437,6 +474,24 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
|
final Ability ability = new AbilityStatic(c, cost, null) { @Override public void resolve() {} };
|
||||||
ability.setActivatingPlayer(c.getController());
|
ability.setActivatingPlayer(c.getController());
|
||||||
|
|
||||||
|
// FIXME: This is a hack to check if the AI can play the "exile from library" pay costs (Cumulative Upkeep,
|
||||||
|
// e.g. Thought Lash). We have to do it and bail early if the AI can't pay, because otherwise the AI will
|
||||||
|
// pay the cost partially, which should not be possible
|
||||||
|
int nExileLib = 0;
|
||||||
|
List<CostPart> parts = CostAdjustment.adjust(cost, sa).getCostParts();
|
||||||
|
for (final CostPart part : parts) {
|
||||||
|
if (part instanceof CostExile) {
|
||||||
|
CostExile exile = (CostExile) part;
|
||||||
|
if (exile.from == ZoneType.Library) {
|
||||||
|
nExileLib += exile.convertAmount();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nExileLib > c.getController().getCardsIn(ZoneType.Library).size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// - End of hack for Exile a card from library Cumulative Upkeep -
|
||||||
|
|
||||||
if (ComputerUtilCost.canPayCost(ability, c.getController())) {
|
if (ComputerUtilCost.canPayCost(ability, c.getController())) {
|
||||||
ComputerUtil.playNoStack(c.getController(), ability, game);
|
ComputerUtil.playNoStack(c.getController(), ability, game);
|
||||||
return true;
|
return true;
|
||||||
@@ -509,7 +564,41 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Boolean defaultVal) {
|
public boolean chooseBinary(SpellAbility sa, String question, BinaryChoiceType kindOfChoice, Boolean defaultVal) {
|
||||||
switch(kindOfChoice) {
|
switch(kindOfChoice) {
|
||||||
case TapOrUntap: return true;
|
case TapOrUntap: return true;
|
||||||
case UntapOrLeaveTapped: return defaultVal != null && defaultVal.booleanValue();
|
case UntapOrLeaveTapped:
|
||||||
|
Card source = sa.getHostCard();
|
||||||
|
if (source != null && source.hasSVar("AIUntapPreference")) {
|
||||||
|
switch (source.getSVar("AIUntapPreference")) {
|
||||||
|
case "Always":
|
||||||
|
return true;
|
||||||
|
case "Never":
|
||||||
|
return false;
|
||||||
|
case "NothingRemembered":
|
||||||
|
if (source.getRememberedCount() == 0) {
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
Card rem = (Card) source.getFirstRemembered();
|
||||||
|
if (!rem.getZone().is(ZoneType.Battlefield)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "BetterTgtThanRemembered":
|
||||||
|
if (source.getRememberedCount() > 0) {
|
||||||
|
Card rem = (Card) source.getFirstRemembered();
|
||||||
|
if (!rem.getZone().is(ZoneType.Battlefield)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
for (Card c : source.getController().getCreaturesInPlay()) {
|
||||||
|
if (c != rem && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(rem) + 30) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultVal != null && defaultVal.booleanValue();
|
||||||
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
|
case UntapTimeVault: return false; // TODO Should AI skip his turn for time vault?
|
||||||
case LeftOrRight: return brains.chooseDirection(sa);
|
case LeftOrRight: return brains.chooseDirection(sa);
|
||||||
default:
|
default:
|
||||||
@@ -796,11 +885,6 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
return brains.getBooleanProperty(AiProps.CHEAT_WITH_MANA_ON_SHUFFLE) ? brains.cheatShuffle(list) : list;
|
return brains.getBooleanProperty(AiProps.CHEAT_WITH_MANA_ON_SHUFFLE) ? brains.cheatShuffle(list) : list;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public CardShields chooseRegenerationShield(Card c) {
|
|
||||||
return Iterables.getFirst(c.getShields(), null);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<PaperCard> chooseCardsYouWonToAddToDeck(List<PaperCard> losses) {
|
public List<PaperCard> chooseCardsYouWonToAddToDeck(List<PaperCard> losses) {
|
||||||
// TODO AI takes all by default
|
// TODO AI takes all by default
|
||||||
@@ -892,6 +976,8 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
} else if (logic.equals("MostProminentSpellInComputerDeck")) {
|
} else if (logic.equals("MostProminentSpellInComputerDeck")) {
|
||||||
CardCollectionView cards = CardLists.getValidCards(aiLibrary, "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());
|
||||||
@@ -913,6 +999,14 @@ public class PlayerControllerAi extends PlayerController {
|
|||||||
return brains.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player, decider);
|
return brains.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player, decider);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<Card> chooseCardsForZoneChange(
|
||||||
|
ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList,
|
||||||
|
DelayedReveal delayedReveal, String selectPrompt, Player decider) {
|
||||||
|
// this isn't used
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void resetAtEndOfTurn() {
|
public void resetAtEndOfTurn() {
|
||||||
// TODO - if card memory is ever used to remember something for longer than a turn, make sure it's not reset here.
|
// TODO - if card memory is ever used to remember something for longer than a turn, make sure it's not reset here.
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,7 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.Collection;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.google.common.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.card.ICardFace;
|
import forge.card.ICardFace;
|
||||||
import forge.card.mana.ManaCost;
|
import forge.card.mana.ManaCost;
|
||||||
import forge.card.mana.ManaCostParser;
|
import forge.card.mana.ManaCostParser;
|
||||||
@@ -25,6 +20,10 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.spellability.SpellAbilityCondition;
|
import forge.game.spellability.SpellAbilityCondition;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class for API-specific AI logic
|
* Base class for API-specific AI logic
|
||||||
* <p>
|
* <p>
|
||||||
@@ -103,6 +102,12 @@ public abstract class SpellAbilityAi {
|
|||||||
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
||||||
*/
|
*/
|
||||||
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 (aiLogic.equals("CheckCondition")) {
|
||||||
|
SpellAbility saCopy = sa.copy();
|
||||||
|
saCopy.setActivatingPlayer(ai);
|
||||||
|
return saCopy.getConditions().areMet(saCopy);
|
||||||
|
}
|
||||||
|
|
||||||
return !("Never".equals(aiLogic));
|
return !("Never".equals(aiLogic));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +123,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)) {
|
||||||
@@ -319,7 +324,7 @@ public abstract class SpellAbilityAi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
|
||||||
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleSpellAbility is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
|
System.err.println("Warning: default (ie. inherited from base class) implementation of chooseSingleSpellAbility is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
|
||||||
return spells.get(0);
|
return spells.get(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package forge.ai;
|
package forge.ai;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import com.google.common.collect.ImmutableMap;
|
import com.google.common.collect.ImmutableMap;
|
||||||
import com.google.common.collect.Maps;
|
import com.google.common.collect.Maps;
|
||||||
|
|
||||||
import forge.ai.ability.*;
|
import forge.ai.ability.*;
|
||||||
import forge.game.ability.ApiType;
|
import forge.game.ability.ApiType;
|
||||||
import forge.util.ReflectionUtil;
|
import forge.util.ReflectionUtil;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public enum SpellApiToAi {
|
public enum SpellApiToAi {
|
||||||
Converter;
|
Converter;
|
||||||
|
|
||||||
@@ -25,12 +24,15 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.Animate, AnimateAi.class)
|
.put(ApiType.Animate, AnimateAi.class)
|
||||||
.put(ApiType.AnimateAll, AnimateAllAi.class)
|
.put(ApiType.AnimateAll, AnimateAllAi.class)
|
||||||
.put(ApiType.Attach, AttachAi.class)
|
.put(ApiType.Attach, AttachAi.class)
|
||||||
|
.put(ApiType.Ascend, AlwaysPlayAi.class)
|
||||||
|
.put(ApiType.AssignGroup, AssignGroupAi.class)
|
||||||
.put(ApiType.Balance, BalanceAi.class)
|
.put(ApiType.Balance, BalanceAi.class)
|
||||||
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
|
.put(ApiType.BecomeMonarch, AlwaysPlayAi.class)
|
||||||
.put(ApiType.BecomesBlocked, BecomesBlockedAi.class)
|
.put(ApiType.BecomesBlocked, BecomesBlockedAi.class)
|
||||||
.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)
|
||||||
@@ -67,16 +69,19 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.Encode, EncodeAi.class)
|
.put(ApiType.Encode, EncodeAi.class)
|
||||||
.put(ApiType.EndTurn, EndTurnAi.class)
|
.put(ApiType.EndTurn, EndTurnAi.class)
|
||||||
.put(ApiType.ExchangeLife, LifeExchangeAi.class)
|
.put(ApiType.ExchangeLife, LifeExchangeAi.class)
|
||||||
|
.put(ApiType.ExchangeLifeVariant, LifeExchangeVariantAi.class)
|
||||||
.put(ApiType.ExchangeControl, ControlExchangeAi.class)
|
.put(ApiType.ExchangeControl, ControlExchangeAi.class)
|
||||||
.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, ExploreAi.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)
|
||||||
.put(ApiType.GainControl, ControlGainAi.class)
|
.put(ApiType.GainControl, ControlGainAi.class)
|
||||||
.put(ApiType.GainLife, LifeGainAi.class)
|
.put(ApiType.GainLife, LifeGainAi.class)
|
||||||
.put(ApiType.GainOwnership, CannotPlayAi.class)
|
.put(ApiType.GainOwnership, CannotPlayAi.class)
|
||||||
|
.put(ApiType.GameDrawn, CannotPlayAi.class)
|
||||||
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
|
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
|
||||||
.put(ApiType.Goad, GoadAi.class)
|
.put(ApiType.Goad, GoadAi.class)
|
||||||
.put(ApiType.Haunt, HauntAi.class)
|
.put(ApiType.Haunt, HauntAi.class)
|
||||||
@@ -114,6 +119,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.RearrangeTopOfLibrary, RearrangeTopOfLibraryAi.class)
|
.put(ApiType.RearrangeTopOfLibrary, RearrangeTopOfLibraryAi.class)
|
||||||
.put(ApiType.Regenerate, RegenerateAi.class)
|
.put(ApiType.Regenerate, RegenerateAi.class)
|
||||||
.put(ApiType.RegenerateAll, RegenerateAllAi.class)
|
.put(ApiType.RegenerateAll, RegenerateAllAi.class)
|
||||||
|
.put(ApiType.Regeneration, AlwaysPlayAi.class)
|
||||||
.put(ApiType.RemoveCounter, CountersRemoveAi.class)
|
.put(ApiType.RemoveCounter, CountersRemoveAi.class)
|
||||||
.put(ApiType.RemoveCounterAll, CannotPlayAi.class)
|
.put(ApiType.RemoveCounterAll, CannotPlayAi.class)
|
||||||
.put(ApiType.RemoveFromCombat, RemoveFromCombatAi.class)
|
.put(ApiType.RemoveFromCombat, RemoveFromCombatAi.class)
|
||||||
@@ -121,6 +127,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.Repeat, RepeatAi.class)
|
.put(ApiType.Repeat, RepeatAi.class)
|
||||||
.put(ApiType.RepeatEach, RepeatEachAi.class)
|
.put(ApiType.RepeatEach, RepeatEachAi.class)
|
||||||
.put(ApiType.ReplaceEffect, AlwaysPlayAi.class)
|
.put(ApiType.ReplaceEffect, AlwaysPlayAi.class)
|
||||||
|
.put(ApiType.ReplaceDamage, AlwaysPlayAi.class)
|
||||||
.put(ApiType.ReplaceSplitDamage, AlwaysPlayAi.class)
|
.put(ApiType.ReplaceSplitDamage, AlwaysPlayAi.class)
|
||||||
.put(ApiType.RestartGame, RestartGameAi.class)
|
.put(ApiType.RestartGame, RestartGameAi.class)
|
||||||
.put(ApiType.Reveal, RevealAi.class)
|
.put(ApiType.Reveal, RevealAi.class)
|
||||||
@@ -151,6 +158,7 @@ public enum SpellApiToAi {
|
|||||||
.put(ApiType.Vote, VoteAi.class)
|
.put(ApiType.Vote, VoteAi.class)
|
||||||
.put(ApiType.WinsGame, GameWinAi.class)
|
.put(ApiType.WinsGame, GameWinAi.class)
|
||||||
|
|
||||||
|
.put(ApiType.DamageResolve, AlwaysPlayAi.class)
|
||||||
.put(ApiType.InternalEtbReplacement, CanPlayAsDrawbackAi.class)
|
.put(ApiType.InternalEtbReplacement, CanPlayAsDrawbackAi.class)
|
||||||
.put(ApiType.InternalLegendaryRule, LegendaryRuleAi.class)
|
.put(ApiType.InternalLegendaryRule, LegendaryRuleAi.class)
|
||||||
.put(ApiType.InternalIgnoreEffect, CannotPlayAi.class)
|
.put(ApiType.InternalIgnoreEffect, CannotPlayAi.class)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Map;
|
||||||
|
|
||||||
public class ActivateAbilityAi extends SpellAbilityAi {
|
public class ActivateAbilityAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -23,8 +23,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 = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
|
||||||
|
|
||||||
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
|
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
@@ -95,7 +94,8 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
|
||||||
|
Map<String, Object> params) {
|
||||||
return spells.get(0);
|
return spells.get(0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.ComputerUtil;
|
|
||||||
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;
|
||||||
@@ -36,8 +23,12 @@ import forge.game.staticability.StaticAbilityLayer;
|
|||||||
import forge.game.trigger.Trigger;
|
import forge.game.trigger.Trigger;
|
||||||
import forge.game.trigger.TriggerHandler;
|
import forge.game.trigger.TriggerHandler;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.collect.FCollectionView;
|
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import forge.game.ability.effects.AnimateEffectBase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
@@ -172,11 +163,17 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
|
||||||
|
if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
|
||||||
bFlag = true;
|
bFlag = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) {
|
if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) {
|
||||||
|
if (sa.hasParam("Crew") && c.isCreature()) {
|
||||||
|
// Do not try to crew a vehicle which is already a creature
|
||||||
|
return false;
|
||||||
|
}
|
||||||
Card animatedCopy = becomeAnimated(c, sa);
|
Card animatedCopy = becomeAnimated(c, sa);
|
||||||
if (ph.isPlayerTurn(aiPlayer)
|
if (ph.isPlayerTurn(aiPlayer)
|
||||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
|
||||||
@@ -186,9 +183,21 @@ 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())) {
|
||||||
|
if (!sa.getHostCard().isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(sa.getHostCard()))) {
|
||||||
|
bFlag = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (bFlag) {
|
||||||
|
this.rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
|
||||||
|
}
|
||||||
return bFlag; // All of the defined stuff is animated, not very useful
|
return bFlag; // All of the defined stuff is animated, not very useful
|
||||||
} else {
|
} else {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -251,13 +260,8 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
// need to targetable
|
// need to targetable
|
||||||
list = CardLists.getTargetableCards(list, sa);
|
list = CardLists.getTargetableCards(list, sa);
|
||||||
|
|
||||||
// try to look for AI targets if able
|
// Filter AI-specific targets if provided
|
||||||
if (sa.hasParam("AITgts")) {
|
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||||
CardCollection prefList = CardLists.getValidCards(list, sa.getParam("AITgts").split(","), ai, source, sa);
|
|
||||||
if(!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
|
||||||
list = prefList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// list is empty, no possible targets
|
// list is empty, no possible targets
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
@@ -301,8 +305,9 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
// if its player turn,
|
// if its player turn,
|
||||||
// check if its Permanent or that creature would attack
|
// check if its Permanent or that creature would attack
|
||||||
if (ph.isPlayerTurn(ai)) {
|
if (ph.isPlayerTurn(ai)) {
|
||||||
if (!sa.hasParam("Permanent") &&
|
if (!sa.hasParam("Permanent")
|
||||||
!ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)) {
|
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
|
||||||
|
&& !sa.hasParam("UntilHostLeavesPlay")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,11 +363,11 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
card.setSickness(hasOriginalCardSickness);
|
card.setSickness(hasOriginalCardSickness);
|
||||||
|
|
||||||
// AF specific sa
|
// AF specific sa
|
||||||
int power = -1;
|
Integer power = null;
|
||||||
if (sa.hasParam("Power")) {
|
if (sa.hasParam("Power")) {
|
||||||
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
|
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
|
||||||
}
|
}
|
||||||
int toughness = -1;
|
Integer toughness = null;
|
||||||
if (sa.hasParam("Toughness")) {
|
if (sa.hasParam("Toughness")) {
|
||||||
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
|
toughness = AbilityUtils.calculateAmount(source, sa.getParam("Toughness"), sa);
|
||||||
}
|
}
|
||||||
@@ -448,59 +453,7 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
|
sVars.addAll(Arrays.asList(sa.getParam("sVars").split(",")));
|
||||||
}
|
}
|
||||||
|
|
||||||
// duplicating AnimateEffectBase.doAnimate
|
AnimateEffectBase.doAnimate(card, sa, power, toughness, types, removeTypes, finalDesc, keywords, removeKeywords, hiddenKeywords, timestamp);
|
||||||
boolean removeSuperTypes = false;
|
|
||||||
boolean removeCardTypes = false;
|
|
||||||
boolean removeSubTypes = false;
|
|
||||||
boolean removeCreatureTypes = false;
|
|
||||||
|
|
||||||
if (sa.hasParam("OverwriteTypes")) {
|
|
||||||
removeSuperTypes = true;
|
|
||||||
removeCardTypes = true;
|
|
||||||
removeSubTypes = true;
|
|
||||||
removeCreatureTypes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.hasParam("KeepSupertypes")) {
|
|
||||||
removeSuperTypes = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.hasParam("KeepCardTypes")) {
|
|
||||||
removeCardTypes = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.hasParam("RemoveSuperTypes")) {
|
|
||||||
removeSuperTypes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.hasParam("RemoveCardTypes")) {
|
|
||||||
removeCardTypes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.hasParam("RemoveSubTypes")) {
|
|
||||||
removeSubTypes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.hasParam("RemoveCreatureTypes")) {
|
|
||||||
removeCreatureTypes = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((power != -1) || (toughness != -1)) {
|
|
||||||
card.addNewPT(power, toughness, timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!types.isEmpty() || !removeTypes.isEmpty() || removeCreatureTypes) {
|
|
||||||
card.addChangedCardTypes(types, removeTypes, removeSuperTypes, removeCardTypes, removeSubTypes,
|
|
||||||
removeCreatureTypes, timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
card.addChangedCardKeywords(keywords, removeKeywords, sa.hasParam("RemoveAllAbilities"), timestamp);
|
|
||||||
|
|
||||||
for (final String k : hiddenKeywords) {
|
|
||||||
card.addHiddenExtrinsicKeyword(k);
|
|
||||||
}
|
|
||||||
|
|
||||||
card.addColor(finalDesc, !sa.hasParam("OverwriteColors"), timestamp);
|
|
||||||
|
|
||||||
// back to duplicating AnimateEffect.resolve
|
// back to duplicating AnimateEffect.resolve
|
||||||
// TODO will all these abilities/triggers/replacements/etc. lead to
|
// TODO will all these abilities/triggers/replacements/etc. lead to
|
||||||
@@ -510,10 +463,14 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
boolean clearAbilities = sa.hasParam("OverwriteAbilities");
|
boolean clearAbilities = sa.hasParam("OverwriteAbilities");
|
||||||
boolean clearSpells = sa.hasParam("OverwriteSpells");
|
boolean clearSpells = sa.hasParam("OverwriteSpells");
|
||||||
boolean removeAll = sa.hasParam("RemoveAllAbilities");
|
boolean removeAll = sa.hasParam("RemoveAllAbilities");
|
||||||
|
boolean removeIntrinsic = sa.hasParam("RemoveIntrinsicAbilities");
|
||||||
|
|
||||||
if (clearAbilities || clearSpells || removeAll) {
|
if (clearAbilities || clearSpells || removeAll) {
|
||||||
for (final SpellAbility ab : card.getSpellAbilities()) {
|
for (final SpellAbility ab : card.getSpellAbilities()) {
|
||||||
if (removeAll || (ab.isAbility() && clearAbilities) || (ab.isSpell() && clearSpells)) {
|
if (removeAll
|
||||||
|
|| (ab.isIntrinsic() && removeIntrinsic && !ab.isBasicLandAbility())
|
||||||
|
|| (ab.isAbility() && clearAbilities)
|
||||||
|
|| (ab.isSpell() && clearSpells)) {
|
||||||
card.removeSpellAbility(ab);
|
card.removeSpellAbility(ab);
|
||||||
removedAbilities.add(ab);
|
removedAbilities.add(ab);
|
||||||
}
|
}
|
||||||
@@ -554,9 +511,11 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// suppress triggers from the animated card
|
// suppress triggers from the animated card
|
||||||
final List<Trigger> removedTriggers = Lists.newArrayList();
|
final List<Trigger> removedTriggers = Lists.newArrayList();
|
||||||
if (sa.hasParam("OverwriteTriggers") || removeAll) {
|
if (sa.hasParam("OverwriteTriggers") || removeAll || removeIntrinsic) {
|
||||||
final FCollectionView<Trigger> triggersToRemove = card.getTriggers();
|
for (final Trigger trigger : card.getTriggers()) {
|
||||||
for (final Trigger trigger : triggersToRemove) {
|
if (removeIntrinsic && !trigger.isIntrinsic()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
trigger.setSuppressed(true);
|
trigger.setSuppressed(true);
|
||||||
removedTriggers.add(trigger);
|
removedTriggers.add(trigger);
|
||||||
}
|
}
|
||||||
@@ -592,9 +551,11 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// suppress static abilities from the animated card
|
// suppress static abilities from the animated card
|
||||||
final List<StaticAbility> removedStatics = Lists.newArrayList();
|
final List<StaticAbility> removedStatics = Lists.newArrayList();
|
||||||
if (sa.hasParam("OverwriteStatics") || removeAll) {
|
if (sa.hasParam("OverwriteStatics") || removeAll || removeIntrinsic) {
|
||||||
final FCollectionView<StaticAbility> staticsToRemove = card.getStaticAbilities();
|
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||||
for (final StaticAbility stAb : staticsToRemove) {
|
if (removeIntrinsic && !stAb.isIntrinsic()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
stAb.setTemporarilySuppressed(true);
|
stAb.setTemporarilySuppressed(true);
|
||||||
removedStatics.add(stAb);
|
removedStatics.add(stAb);
|
||||||
}
|
}
|
||||||
@@ -602,8 +563,11 @@ public class AnimateAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// suppress static abilities from the animated card
|
// suppress static abilities from the animated card
|
||||||
final List<ReplacementEffect> removedReplacements = Lists.newArrayList();
|
final List<ReplacementEffect> removedReplacements = Lists.newArrayList();
|
||||||
if (sa.hasParam("OverwriteReplacements") || removeAll) {
|
if (sa.hasParam("OverwriteReplacements") || removeAll || removeIntrinsic) {
|
||||||
for (final ReplacementEffect re : card.getReplacementEffects()) {
|
for (final ReplacementEffect re : card.getReplacementEffects()) {
|
||||||
|
if (removeIntrinsic && !re.isIntrinsic()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
re.setTemporarilySuppressed(true);
|
re.setTemporarilySuppressed(true);
|
||||||
removedReplacements.add(re);
|
removedReplacements.add(re);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ public class AnimateAllAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||||
return false;
|
return mandatory;
|
||||||
}
|
}
|
||||||
|
|
||||||
} // end class AbilityFactoryAnimate
|
} // end class AbilityFactoryAnimate
|
||||||
|
|||||||
33
forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java
Normal file
33
forge-ai/src/main/java/forge/ai/ability/AssignGroupAi.java
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
|
import forge.ai.SpellAbilityAi;
|
||||||
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
|
public class AssignGroupAi extends SpellAbilityAi {
|
||||||
|
|
||||||
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
|
// TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
|
||||||
|
// otherwise the AI considers the card playable.
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
|
if (logic.equals("FriendOrFoe")) {
|
||||||
|
if (params.containsKey("Affected") && spells.size() >= 2) {
|
||||||
|
Player t = (Player) params.get("Affected");
|
||||||
|
return spells.get(player.isOpponentOf(t) ? 1 : 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Iterables.getFirst(spells, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,17 +2,18 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
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.ai.*;
|
import forge.ai.*;
|
||||||
import forge.game.GameObject;
|
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.*;
|
import forge.game.card.*;
|
||||||
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
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.CostSacrifice;
|
import forge.game.cost.CostSacrifice;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -24,7 +25,10 @@ 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 java.util.*;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class AttachAi extends SpellAbilityAi {
|
public class AttachAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -36,9 +40,15 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
final Cost abCost = sa.getPayCosts();
|
final Cost abCost = sa.getPayCosts();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
|
// TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures
|
||||||
|
// and gaining card advantage
|
||||||
|
if (source.hasKeyword("MayFlashSac") && !ai.couldCastSorcery(sa)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
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)) {
|
||||||
@@ -85,7 +95,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
final CardCollection targets = CardLists.filter(list, new Predicate<Card>() {
|
final CardCollection targets = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof"));
|
return !(c.hasProtectionFrom(source) || c.hasKeyword(Keyword.SHROUD) || c.hasKeyword(Keyword.HEXPROOF));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (targets.isEmpty()) {
|
if (targets.isEmpty()) {
|
||||||
@@ -246,10 +256,22 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
// Don't do Untapped Vigilance cards
|
// Don't do Untapped Vigilance cards
|
||||||
if (c.isCreature() && c.hasKeyword("Vigilance") && c.isUntapped()) {
|
if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -376,7 +398,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
List<Card> evenBetterList = CardLists.filter(betterList, new Predicate<Card>() {
|
List<Card> evenBetterList = CardLists.filter(betterList, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return c.hasKeyword("Indestructible") || c.hasKeyword("Hexproof");
|
return c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.hasKeyword(Keyword.HEXPROOF);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!evenBetterList.isEmpty()) {
|
if (!evenBetterList.isEmpty()) {
|
||||||
@@ -455,6 +477,65 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return c;
|
return c;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cards that trigger on dealing damage
|
||||||
|
private static Card attachAICuriosityPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
||||||
|
final Card attachSource) {
|
||||||
|
Card chosen = null;
|
||||||
|
int priority = 0;
|
||||||
|
for (Card card : list) {
|
||||||
|
int cardPriority = 0;
|
||||||
|
// Prefer Evasion
|
||||||
|
if (card.hasKeyword(Keyword.TRAMPLE)) {
|
||||||
|
cardPriority += 10;
|
||||||
|
}
|
||||||
|
if (card.hasKeyword(Keyword.MENACE)) {
|
||||||
|
cardPriority += 10;
|
||||||
|
}
|
||||||
|
// Avoid this for Sleepers Robe?
|
||||||
|
if (card.hasKeyword(Keyword.FEAR)) {
|
||||||
|
cardPriority += 15;
|
||||||
|
}
|
||||||
|
if (card.hasKeyword(Keyword.FLYING)) {
|
||||||
|
cardPriority += 20;
|
||||||
|
}
|
||||||
|
if (card.hasKeyword(Keyword.SHADOW)) {
|
||||||
|
cardPriority += 30;
|
||||||
|
}
|
||||||
|
if (card.hasKeyword(Keyword.HORSEMANSHIP)) {
|
||||||
|
cardPriority += 40;
|
||||||
|
}
|
||||||
|
if (card.hasKeyword("Unblockable")) {
|
||||||
|
cardPriority += 50;
|
||||||
|
}
|
||||||
|
// Prefer "tap to deal damage"
|
||||||
|
// TODO : Skip this one if triggers on combat damage only?
|
||||||
|
for (SpellAbility sa2 : card.getSpellAbilities()) {
|
||||||
|
if (ApiType.DealDamage.equals(sa2.getApi())
|
||||||
|
&& (sa2.getTargetRestrictions().canTgtPlayer())) {
|
||||||
|
cardPriority += 300;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Prefer stronger creatures, avoid if can't attack
|
||||||
|
cardPriority += card.getCurrentToughness() * 2;
|
||||||
|
cardPriority += card.getCurrentPower();
|
||||||
|
if (card.getCurrentPower() <= 0) {
|
||||||
|
cardPriority = -100;
|
||||||
|
}
|
||||||
|
if (card.hasKeyword(Keyword.DEFENDER)) {
|
||||||
|
cardPriority = -100;
|
||||||
|
}
|
||||||
|
if (card.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
|
cardPriority += 15;
|
||||||
|
}
|
||||||
|
if (cardPriority > priority) {
|
||||||
|
priority = cardPriority;
|
||||||
|
chosen = card;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return chosen;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* Attach ai specific card preference.
|
* Attach ai specific card preference.
|
||||||
*
|
*
|
||||||
@@ -478,7 +559,9 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
if ("Guilty Conscience".equals(sourceName)) {
|
if ("Guilty Conscience".equals(sourceName)) {
|
||||||
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
|
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
|
||||||
} else if ("Bonds of Faith".equals(sourceName)) {
|
} else if ("Bonds of Faith".equals(sourceName)) {
|
||||||
chosen = SpecialCardAi.BondsOfFaith.getBestAttachTarget(ai, sa, list);
|
chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
|
||||||
|
} else if ("Clutch of Undeath".equals(sourceName)) {
|
||||||
|
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
|
||||||
}
|
}
|
||||||
|
|
||||||
// If Mandatory (brought directly into play without casting) gotta
|
// If Mandatory (brought directly into play without casting) gotta
|
||||||
@@ -490,6 +573,44 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return chosen;
|
return chosen;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Card attachAIInstantReequipPreference(final SpellAbility sa, final Card attachSource) {
|
||||||
|
// e.g. Cranial Plating
|
||||||
|
PhaseHandler ph = attachSource.getGame().getPhaseHandler();
|
||||||
|
Combat combat = attachSource.getGame().getCombat();
|
||||||
|
Card equipped = sa.getHostCard().getEquipping();
|
||||||
|
if (equipped == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int powerBuff = 0;
|
||||||
|
for (StaticAbility stAb : sa.getHostCard().getStaticAbilities()) {
|
||||||
|
if ("Card.EquippedBy".equals(stAb.getParam("Affected")) && stAb.hasParam("AddPower")) {
|
||||||
|
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
|
||||||
|
int damage = 0;
|
||||||
|
for (Card c : combat.getUnblockedAttackers()) {
|
||||||
|
damage += ComputerUtilCombat.predictDamageTo(combat.getDefenderPlayerByAttacker(equipped), c.getNetCombatDamage(), c, true);
|
||||||
|
}
|
||||||
|
if (combat.isBlocked(equipped)) {
|
||||||
|
for (Card atk : combat.getAttackers()) {
|
||||||
|
if (!combat.isBlocked(atk) && !ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), null).contains(atk)) {
|
||||||
|
if (ComputerUtilCombat.predictDamageTo(combat.getDefenderPlayerByAttacker(atk),
|
||||||
|
atk.getNetCombatDamage(), atk, true) > 0) {
|
||||||
|
if (damage + powerBuff >= combat.getDefenderPlayerByAttacker(atk).getLife()) {
|
||||||
|
sa.resetTargets(); // this is needed to avoid bugs with adding two targets to a single SA
|
||||||
|
return atk; // lethal damage, we can win right now, so why not?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Should generalize this code a bit since they all have similar structures
|
// Should generalize this code a bit since they all have similar structures
|
||||||
/**
|
/**
|
||||||
* Attach ai control preference.
|
* Attach ai control preference.
|
||||||
@@ -605,7 +726,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
prefList = CardLists.filter(list, new Predicate<Card>() {
|
prefList = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
if (!c.hasKeyword("Indestructible") && (c.getLethalDamage() <= Math.abs(tgh))) {
|
if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && (c.getLethalDamage() <= Math.abs(tgh))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -850,8 +971,8 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
|
||||||
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
|
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
|
||||||
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
|
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
|
||||||
|
|
||||||
grantingAbilities |= stabMap.containsKey("AddAbility");
|
grantingAbilities |= stabMap.containsKey("AddAbility");
|
||||||
|
|
||||||
@@ -892,7 +1013,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
if (isUsefulAttachKeyword(keyword, c, sa, pow)) {
|
if (isUsefulAttachKeyword(keyword, c, sa, pow)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (c.hasKeyword("Infect") && pow >= 2) {
|
if (c.hasKeyword(Keyword.INFECT) && pow >= 2) {
|
||||||
// consider +2 power a significant bonus on Infect creatures
|
// consider +2 power a significant bonus on Infect creatures
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -920,7 +1041,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
prefList = CardLists.filter(prefList, new Predicate<Card>() {
|
prefList = CardLists.filter(prefList, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return !c.isEnchanted() || c.hasKeyword("Hexproof");
|
return !c.isEnchanted() || c.hasKeyword(Keyword.HEXPROOF);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -1022,9 +1143,9 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
CardCollection prefList = list;
|
CardCollection prefList = list;
|
||||||
if (sa.hasParam("AITgts")) {
|
|
||||||
prefList = CardLists.getValidCards(list, sa.getParam("AITgts"), sa.getActivatingPlayer(), attachSource);
|
// Filter AI-specific targets if provided
|
||||||
}
|
prefList = ComputerUtil.filterAITgts(sa, aiPlayer, (CardCollection)list, true);
|
||||||
|
|
||||||
Card c = attachGeneralAI(aiPlayer, sa, prefList, mandatory, attachSource, sa.getParam("AILogic"));
|
Card c = attachGeneralAI(aiPlayer, sa, prefList, mandatory, attachSource, sa.getParam("AILogic"));
|
||||||
|
|
||||||
@@ -1037,6 +1158,10 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("InstantReequipPowerBuff".equals(sa.getParam("AILogic"))) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
boolean uselessCreature = ComputerUtilCard.isUselessCreature(aiPlayer, attachSource.getEquipping());
|
boolean uselessCreature = ComputerUtilCard.isUselessCreature(aiPlayer, attachSource.getEquipping());
|
||||||
|
|
||||||
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) {
|
if (aic.getProperty(AiProps.MOVE_EQUIPMENT_TO_BETTER_CREATURES).equals("never")) {
|
||||||
@@ -1053,10 +1178,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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1099,7 +1224,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
private static Card attachGeneralAI(final Player ai, final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
private static Card attachGeneralAI(final Player ai, final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
||||||
final Card attachSource, final String logic) {
|
final Card attachSource, final String logic) {
|
||||||
Player prefPlayer = ai.getWeakestOpponent();
|
Player prefPlayer = ai.getWeakestOpponent();
|
||||||
if ("Pump".equals(logic) || "Animate".equals(logic)) {
|
if ("Pump".equals(logic) || "Animate".equals(logic) || "Curiosity".equals(logic)) {
|
||||||
prefPlayer = ai;
|
prefPlayer = ai;
|
||||||
}
|
}
|
||||||
// Some ChangeType cards are beneficial, and PrefPlayer should be
|
// Some ChangeType cards are beneficial, and PrefPlayer should be
|
||||||
@@ -1113,6 +1238,12 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
prefList = CardLists.filterControlledBy(list, prefPlayer);
|
prefList = CardLists.filterControlledBy(list, prefPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AI logic types that do not require a prefList and that evaluate the
|
||||||
|
// usefulness of attach action autonomously
|
||||||
|
if ("InstantReequipPowerBuff".equals(logic)) {
|
||||||
|
return attachAIInstantReequipPreference(sa, attachSource);
|
||||||
|
}
|
||||||
|
|
||||||
// If there are no preferred cards, and not mandatory bail out
|
// If there are no preferred cards, and not mandatory bail out
|
||||||
if (prefList.isEmpty()) {
|
if (prefList.isEmpty()) {
|
||||||
return chooseUnpreferred(mandatory, list);
|
return chooseUnpreferred(mandatory, list);
|
||||||
@@ -1127,6 +1258,8 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
c = attachAICursePreference(sa, prefList, mandatory, attachSource);
|
c = attachAICursePreference(sa, prefList, mandatory, attachSource);
|
||||||
} else if ("Pump".equals(logic)) {
|
} else if ("Pump".equals(logic)) {
|
||||||
c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource);
|
c = attachAIPumpPreference(ai, sa, prefList, mandatory, attachSource);
|
||||||
|
} else if ("Curiosity".equals(logic)) {
|
||||||
|
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
|
||||||
} else if ("ChangeType".equals(logic)) {
|
} else if ("ChangeType".equals(logic)) {
|
||||||
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
|
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
|
||||||
} else if ("KeepTapped".equals(logic)) {
|
} else if ("KeepTapped".equals(logic)) {
|
||||||
@@ -1185,6 +1318,22 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't play if would choose a color the target is already protected from
|
||||||
|
if (sa.getHostCard().hasSVar("ChosenProtection")) {
|
||||||
|
CardCollectionView oppAllCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||||
|
String cc = ComputerUtilCard.getMostProminentColor(oppAllCards);
|
||||||
|
if (card.hasKeyword("Protection from " + cc.toLowerCase())) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Also don't play if it would destroy own Aura
|
||||||
|
for (Card c : card.getEnchantedBy(false)) {
|
||||||
|
if ((c.getController().equals(ai)) && (c.isOfColor(cc))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final boolean evasive = (keyword.equals("Unblockable") || keyword.equals("Fear")
|
final boolean evasive = (keyword.equals("Unblockable") || keyword.equals("Fear")
|
||||||
|| keyword.equals("Intimidate") || keyword.equals("Shadow")
|
|| keyword.equals("Intimidate") || keyword.equals("Shadow")
|
||||||
|| keyword.equals("Flying") || keyword.equals("Horsemanship")
|
|| keyword.equals("Flying") || keyword.equals("Horsemanship")
|
||||||
@@ -1227,7 +1376,7 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("First Strike")) {
|
} else if (keyword.equals("First Strike")) {
|
||||||
if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword("Double Strike")
|
if (card.getNetCombatDamage() + powerBonus <= 0 || card.hasKeyword(Keyword.DOUBLE_STRIKE)
|
||||||
|| (!ComputerUtilCombat.canAttackNextTurn(card) && !CombatUtil.canBlock(card, true))) {
|
|| (!ComputerUtilCombat.canAttackNextTurn(card) && !CombatUtil.canBlock(card, true))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1260,20 +1409,20 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("Reach")) {
|
} else if (keyword.equals("Reach")) {
|
||||||
if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) {
|
if (card.hasKeyword(Keyword.FLYING) || !CombatUtil.canBlock(card, true)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) {
|
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
|
||||||
if (!CombatUtil.canBlock(card, true) || card.hasKeyword("CARDNAME can block any number of creatures.")
|
if (!CombatUtil.canBlock(card, true) || card.hasKeyword("CARDNAME can block any number of creatures.")
|
||||||
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures.")) {
|
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {
|
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {
|
||||||
if (!card.hasKeyword("Defender") || card.getNetCombatDamage() + powerBonus <= 0) {
|
if (!card.hasKeyword(Keyword.DEFENDER) || card.getNetCombatDamage() + powerBonus <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("Shroud") || keyword.equals("Hexproof")) {
|
} else if (keyword.equals("Shroud") || keyword.equals("Hexproof")) {
|
||||||
if (card.hasKeyword("Shroud") || card.hasKeyword("Hexproof")) {
|
if (card.hasKeyword(Keyword.SHROUD) || card.hasKeyword(Keyword.HEXPROOF)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("Defender")) {
|
} else if (keyword.equals("Defender")) {
|
||||||
@@ -1362,6 +1511,43 @@ public class AttachAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static Card doPumpOrCurseAILogic(final Player ai, final SpellAbility sa, final List<Card> list, final String type) {
|
||||||
|
Card chosen = null;
|
||||||
|
|
||||||
|
List<Card> aiType = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(final Card c) {
|
||||||
|
// Don't buff opponent's creatures of given type
|
||||||
|
if (!c.getController().equals(ai)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return c.getType().hasCreatureType(type);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(final Card c) {
|
||||||
|
// Don't debuff AI's own creatures not of given type
|
||||||
|
if (c.getController().equals(ai)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!aiType.isEmpty() && !oppNonType.isEmpty()) {
|
||||||
|
Card bestAi = ComputerUtilCard.getBestCreatureAI(aiType);
|
||||||
|
Card bestOpp = ComputerUtilCard.getBestCreatureAI(oppNonType);
|
||||||
|
chosen = ComputerUtilCard.evaluateCreature(bestAi) > ComputerUtilCard.evaluateCreature(bestOpp) ? bestAi : bestOpp;
|
||||||
|
} else if (!aiType.isEmpty()) {
|
||||||
|
chosen = ComputerUtilCard.getBestCreatureAI(aiType);
|
||||||
|
} else if (!oppNonType.isEmpty()) {
|
||||||
|
chosen = ComputerUtilCard.getBestCreatureAI(oppNonType);
|
||||||
|
}
|
||||||
|
|
||||||
|
return chosen;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import forge.game.Game;
|
|||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -30,7 +31,7 @@ public class BecomesBlockedAi extends SpellAbilityAi {
|
|||||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
|
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), aiPlayer.getOpponents());
|
||||||
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
||||||
list = CardLists.getTargetableCards(list, sa);
|
list = CardLists.getTargetableCards(list, sa);
|
||||||
list = CardLists.getNotKeyword(list, "Trample");
|
list = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
|
||||||
|
|
||||||
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) {
|
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(source, sa)) {
|
||||||
Card choice = null;
|
Card choice = null;
|
||||||
|
|||||||
@@ -36,10 +36,10 @@ 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.
|
||||||
*/
|
*/
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import forge.game.player.Player;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
|
public class CanPlayAsDrawbackAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -37,7 +38,8 @@ public class CanPlayAsDrawbackAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
|
||||||
|
Map<String, Object> params) {
|
||||||
// This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
|
// This might be called from CopySpellAbilityEffect - to hide warning (for having no overload) use this simple overload
|
||||||
return spells.get(0);
|
return spells.get(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +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 forge.game.card.*;
|
|
||||||
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;
|
||||||
@@ -23,6 +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.*;
|
||||||
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;
|
||||||
@@ -36,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 {
|
||||||
/*
|
/*
|
||||||
@@ -45,6 +38,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
* 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")) {
|
||||||
@@ -60,23 +57,49 @@ 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("ReanimateAll")) {
|
||||||
|
return SpecialCardAi.LivingDeath.consider(aiPlayer, sa);
|
||||||
|
} else if (aiLogic.equals("TheScarabGod")) {
|
||||||
|
return SpecialCardAi.TheScarabGod.consider(aiPlayer, sa);
|
||||||
|
} else if (aiLogic.equals("Intuition")) {
|
||||||
|
// This logic only fills the multiple cards array, the decision to play is made
|
||||||
|
// separately in hiddenOriginCanPlayAI later.
|
||||||
|
multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (isHidden(sa)) {
|
if (isHidden(sa)) {
|
||||||
@@ -129,6 +152,23 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return doReturnCommanderLogic(sa, aiPlayer);
|
return doReturnCommanderLogic(sa, aiPlayer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("Always".equals(sa.getParam("AILogic"))) {
|
||||||
|
return true;
|
||||||
|
} else if ("IfNotBuffed".equals(sa.getParam("AILogic"))) {
|
||||||
|
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
|
||||||
|
return true; // debuffed by opponent's auras to the level that it becomes useless
|
||||||
|
}
|
||||||
|
int delta = 0;
|
||||||
|
for (Card enc : sa.getHostCard().getEnchantedBy(false)) {
|
||||||
|
if (enc.getController().isOpponentOf(aiPlayer)) {
|
||||||
|
delta--;
|
||||||
|
} else {
|
||||||
|
delta++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return delta <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
if (isHidden(sa)) {
|
if (isHidden(sa)) {
|
||||||
return hiddenTriggerAI(aiPlayer, sa, mandatory);
|
return hiddenTriggerAI(aiPlayer, sa, mandatory);
|
||||||
}
|
}
|
||||||
@@ -178,7 +218,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;
|
||||||
}
|
}
|
||||||
@@ -677,6 +717,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") || aiLogic.equals("AtOppEOT")) {
|
||||||
|
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||||
|
}
|
||||||
|
|
||||||
if (isHidden(sa)) {
|
if (isHidden(sa)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -731,6 +777,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
* @return a boolean.
|
* @return a boolean.
|
||||||
*/
|
*/
|
||||||
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
|
||||||
|
if ("MimicVat".equals(sa.getParam("AILogic"))) {
|
||||||
|
return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa);
|
||||||
|
}
|
||||||
|
|
||||||
if (!sa.usesTargeting()) {
|
if (!sa.usesTargeting()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -775,9 +825,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, source, sa);
|
CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, source, sa);
|
||||||
list = CardLists.getTargetableCards(list, sa);
|
list = CardLists.getTargetableCards(list, sa);
|
||||||
if (sa.hasParam("AITgts")) {
|
|
||||||
list = CardLists.getValidCards(list, sa.getParam("AITgts"), ai, source);
|
// Filter AI-specific targets if provided
|
||||||
|
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||||
|
if (sa.hasParam("AITgtsOnlyBetterThanSelf")) {
|
||||||
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return ComputerUtilCard.evaluateCreature(card) > ComputerUtilCard.evaluateCreature(source) + 30;
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (source.isInZone(ZoneType.Hand)) {
|
if (source.isInZone(ZoneType.Hand)) {
|
||||||
list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(source.getName()))); // Don't get the same card back.
|
list = CardLists.filter(list, Predicates.not(CardPredicates.nameEquals(source.getName()))); // Don't get the same card back.
|
||||||
}
|
}
|
||||||
@@ -985,6 +1044,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if (!sa.hasParam("AITgtOwnCards")) {
|
||||||
list = CardLists.filterControlledBy(list, ai.getOpponents());
|
list = CardLists.filterControlledBy(list, ai.getOpponents());
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -999,6 +1060,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// See if maybe there's a special priority applicable for this, in case the opponent
|
||||||
|
// has dangerous unblockables in play
|
||||||
|
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||||
|
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Only care about combatants during combat
|
// Only care about combatants during combat
|
||||||
if (game.getPhaseHandler().inCombat() && origin.contains(ZoneType.Battlefield)) {
|
if (game.getPhaseHandler().inCombat() && origin.contains(ZoneType.Battlefield)) {
|
||||||
CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null);
|
CardCollection newList = CardLists.getValidCards(list, "Card.attacking,Card.blocking", null, null);
|
||||||
@@ -1011,6 +1079,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;
|
||||||
}
|
}
|
||||||
@@ -1088,8 +1160,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
|
||||||
|
boolean aiTgtsOK = false;
|
||||||
|
if (sa.hasParam("AIMinTgts")) {
|
||||||
|
int minTgts = Integer.parseInt(sa.getParam("AIMinTgts"));
|
||||||
|
if (sa.getTargets().getNumTargeted() >= minTgts) {
|
||||||
|
aiTgtsOK = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!aiTgtsOK) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1295,6 +1376,12 @@ 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);
|
||||||
|
} else if ("ExtraplanarLens".equals(sa.getParam("AILogic"))) {
|
||||||
|
return SpecialCardAi.ExtraplanarLens.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")) {
|
||||||
@@ -1324,7 +1411,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
String logic = sa.getParam("AILogic");
|
String logic = sa.getParam("AILogic");
|
||||||
if ("NeverBounceItself".equals(logic)) {
|
if ("NeverBounceItself".equals(logic)) {
|
||||||
Card source = sa.getHostCard();
|
Card source = sa.getHostCard();
|
||||||
if (fetchList.contains(source)) {
|
if (fetchList.contains(source) && (fetchList.size() > 1 && !sa.getTriggeringAbility().isMandatory())) {
|
||||||
// For cards that should never be bounced back to hand with their own [e.g. triggered] abilities, such as guild lands.
|
// For cards that should never be bounced back to hand with their own [e.g. triggered] abilities, such as guild lands.
|
||||||
fetchList.remove(source);
|
fetchList.remove(source);
|
||||||
}
|
}
|
||||||
@@ -1332,6 +1419,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
return ComputerUtilCard.getWorstAI(fetchList);
|
return ComputerUtilCard.getWorstAI(fetchList);
|
||||||
} else if ("Mairsil".equals(logic)) {
|
} else if ("Mairsil".equals(logic)) {
|
||||||
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
|
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()) {
|
||||||
@@ -1472,7 +1567,33 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
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");
|
||||||
@@ -1617,7 +1738,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
|||||||
// A blink effect implemented using ChangeZone API
|
// A blink effect implemented using ChangeZone API
|
||||||
return false;
|
return false;
|
||||||
} else if (subApi == ApiType.DelayedTrigger) {
|
} else if (subApi == ApiType.DelayedTrigger) {
|
||||||
SpellAbility exec = causeSub.getAdditonalAbility("Execute");
|
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
|
||||||
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
|
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
|
||||||
if ("Exile".equals(exec.getParam("Origin")) && "Battlefield".equals(exec.getParam("Destination"))) {
|
if ("Exile".equals(exec.getParam("Origin")) && "Battlefield".equals(exec.getParam("Destination"))) {
|
||||||
// A blink effect implemented using a delayed trigger
|
// A blink effect implemented using a delayed trigger
|
||||||
@@ -1636,6 +1757,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,33 +1,23 @@
|
|||||||
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;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
|
import forge.game.player.PlayerCollection;
|
||||||
import forge.game.player.PlayerPredicates;
|
import forge.game.player.PlayerPredicates;
|
||||||
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 java.util.Collections;
|
||||||
|
|
||||||
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) {
|
||||||
@@ -46,13 +36,16 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source)) {
|
||||||
|
boolean aiLogicAllowsDiscard = sa.hasParam("AILogic") && sa.getParam("AILogic").startsWith("DiscardAll");
|
||||||
|
|
||||||
|
if (!aiLogicAllowsDiscard) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
boolean chance = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
// TODO targeting with ChangeZoneAll
|
// TODO targeting with ChangeZoneAll
|
||||||
// really two types of targeting.
|
// really two types of targeting.
|
||||||
@@ -82,13 +75,46 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
} else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) {
|
} else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) {
|
||||||
// e.g. Shadow of the Grave
|
// e.g. Shadow of the Grave
|
||||||
return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||||
|
} else if ("ExileGraveyards".equals(sa.getParam("AILogic"))) {
|
||||||
|
for (Player opp : ai.getOpponents()) {
|
||||||
|
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
|
||||||
|
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES);
|
||||||
|
|
||||||
|
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
} else if ("ManifestCreatsFromGraveyard".equals(sa.getParam("AILogic"))) {
|
||||||
|
PlayerCollection players = new PlayerCollection();
|
||||||
|
players.addAll(ai.getOpponents());
|
||||||
|
players.add(ai);
|
||||||
|
int maxSize = 1;
|
||||||
|
for (Player player : players) {
|
||||||
|
Player bestTgt = null;
|
||||||
|
if (player.canBeTargetedBy(sa)) {
|
||||||
|
CardCollectionView cardsGY = CardLists.filter(player.getCardsIn(ZoneType.Graveyard),
|
||||||
|
CardPredicates.Presets.CREATURES);
|
||||||
|
if (cardsGY.size() > maxSize) {
|
||||||
|
maxSize = cardsGY.size();
|
||||||
|
bestTgt = player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bestTgt != null) {
|
||||||
|
sa.resetTargets();
|
||||||
|
sa.getTargets().add(bestTgt);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO improve restrictions on when the AI would want to use this
|
// TODO improve restrictions on when the AI would want to use this
|
||||||
// spBounceAll has some AI we can compare to.
|
// spBounceAll has some AI we can compare to.
|
||||||
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
|
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
|
||||||
if (!sa.usesTargeting()) {
|
if (!sa.usesTargeting()) {
|
||||||
// TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar, Timetwister)
|
// TODO: improve logic for non-targeted SAs of this type (most are currently RemAIDeck, e.g. Memory Jar)
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// search targetable Opponents
|
// search targetable Opponents
|
||||||
@@ -131,15 +157,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;
|
||||||
}
|
}
|
||||||
@@ -181,6 +231,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)) {
|
||||||
@@ -223,7 +281,7 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (((r.nextFloat() < .8) || sa.isTrigger()) && chance);
|
return (((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -232,8 +290,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.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,15 +1,7 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
import forge.ai.*;
|
||||||
import forge.ai.AiController;
|
|
||||||
import forge.ai.AiPlayDecision;
|
|
||||||
import forge.ai.ComputerUtilAbility;
|
|
||||||
import forge.ai.PlayerControllerAi;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.ability.effects.CharmEffect;
|
import forge.game.ability.effects.CharmEffect;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.AbilitySub;
|
import forge.game.spellability.AbilitySub;
|
||||||
@@ -18,6 +10,8 @@ import forge.util.Aggregates;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class CharmAi extends SpellAbilityAi {
|
public class CharmAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
|
||||||
@@ -26,8 +20,6 @@ public class CharmAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
|
|
||||||
final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
|
final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
|
||||||
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
|
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
|
||||||
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
|
boolean timingRight = sa.isTrigger(); //is there a reason to play the charm now?
|
||||||
@@ -70,7 +62,7 @@ public class CharmAi extends SpellAbilityAi {
|
|||||||
sa.setChosenList(chosenList);
|
sa.setChosenList(chosenList);
|
||||||
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
return r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<AbilitySub> chooseOptionsAi(List<AbilitySub> choices, final Player ai, boolean isTrigger, int num,
|
private List<AbilitySub> chooseOptionsAi(List<AbilitySub> choices, final Player ai, boolean isTrigger, int num,
|
||||||
@@ -124,6 +116,8 @@ public class CharmAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
private List<AbilitySub> chooseTriskaidekaphobia(List<AbilitySub> choices, final Player ai) {
|
private List<AbilitySub> chooseTriskaidekaphobia(List<AbilitySub> choices, final Player ai) {
|
||||||
List<AbilitySub> chosenList = Lists.newArrayList();
|
List<AbilitySub> chosenList = Lists.newArrayList();
|
||||||
|
if (choices == null || choices.isEmpty()) { return chosenList; }
|
||||||
|
|
||||||
AbilitySub gain = choices.get(0);
|
AbilitySub gain = choices.get(0);
|
||||||
AbilitySub lose = choices.get(1);
|
AbilitySub lose = choices.get(1);
|
||||||
FCollection<Player> opponents = ai.getOpponents();
|
FCollection<Player> opponents = ai.getOpponents();
|
||||||
|
|||||||
@@ -18,9 +18,11 @@ import forge.game.card.Card;
|
|||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
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.card.CounterType;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseType;
|
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;
|
||||||
@@ -125,8 +127,8 @@ 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 = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||||
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
|
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
|
||||||
oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");
|
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
|
||||||
|
|
||||||
// Use it as a wrath, when the human creatures threat the ai's life
|
// Use it as a wrath, when the human creatures threat the ai's life
|
||||||
if (aiCreatures.isEmpty() && ComputerUtilCombat.sumDamageIfUnblocked(oppCreatures, ai) >= ai.getLife()) {
|
if (aiCreatures.isEmpty() && ComputerUtilCombat.sumDamageIfUnblocked(oppCreatures, ai) >= ai.getLife()) {
|
||||||
@@ -230,6 +232,13 @@ public class ChooseCardAi extends SpellAbilityAi {
|
|||||||
Collections.reverse(creats);
|
Collections.reverse(creats);
|
||||||
choice = creats.get(0);
|
choice = creats.get(0);
|
||||||
}
|
}
|
||||||
|
} else if ("NegativePowerFirst".equals(logic)) {
|
||||||
|
Card lowest = Aggregates.itemWithMin(options, CardPredicates.Accessors.fnGetNetPower);
|
||||||
|
if (lowest.getNetPower() <= 0) {
|
||||||
|
choice = lowest;
|
||||||
|
} else {
|
||||||
|
choice = ComputerUtilCard.getBestCreatureAI(options);
|
||||||
|
}
|
||||||
} else if ("TangleWire".equals(logic)) {
|
} else if ("TangleWire".equals(logic)) {
|
||||||
CardCollectionView betterList = CardLists.filter(options, new Predicate<Card>() {
|
CardCollectionView betterList = CardLists.filter(options, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -253,7 +262,7 @@ public class ChooseCardAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else if (logic.equals("Duneblast")) {
|
} else if (logic.equals("Duneblast")) {
|
||||||
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
|
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
|
||||||
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
|
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
|
||||||
|
|
||||||
if (aiCreatures.isEmpty()) {
|
if (aiCreatures.isEmpty()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -261,6 +270,10 @@ public class ChooseCardAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
Card chosen = ComputerUtilCard.getBestCreatureAI(aiCreatures);
|
Card chosen = ComputerUtilCard.getBestCreatureAI(aiCreatures);
|
||||||
return chosen;
|
return chosen;
|
||||||
|
} else if (logic.equals("OrzhovAdvokist")) {
|
||||||
|
if (ai.equals(sa.getActivatingPlayer())) {
|
||||||
|
choice = ComputerUtilCard.getBestAI(options);
|
||||||
|
} // TODO: improve ai
|
||||||
} else {
|
} else {
|
||||||
choice = ComputerUtilCard.getBestAI(options);
|
choice = ComputerUtilCard.getBestAI(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -17,7 +14,6 @@ import forge.card.CardStateName;
|
|||||||
import forge.card.ICardFace;
|
import forge.card.ICardFace;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardUtil;
|
import forge.game.card.CardUtil;
|
||||||
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.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
@@ -36,22 +32,9 @@ 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();
|
||||||
@@ -78,6 +61,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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,9 +88,9 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
|||||||
Card copy = CardUtil.getLKICopy(card);
|
Card copy = CardUtil.getLKICopy(card);
|
||||||
// for calcing i need only one split side
|
// for calcing i need only one split side
|
||||||
if (isOther) {
|
if (isOther) {
|
||||||
copy.getCurrentState().copyFrom(card, card.getState(CardStateName.RightSplit));
|
copy.getCurrentState().copyFrom(card.getState(CardStateName.RightSplit), true);
|
||||||
} else {
|
} else {
|
||||||
copy.getCurrentState().copyFrom(card, card.getState(CardStateName.LeftSplit));
|
copy.getCurrentState().copyFrom(card.getState(CardStateName.LeftSplit), true);
|
||||||
}
|
}
|
||||||
copy.updateStateForView();
|
copy.updateStateForView();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
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;
|
||||||
@@ -79,7 +80,8 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
|
||||||
|
Map<String, Object> params) {
|
||||||
Card host = sa.getHostCard();
|
Card host = sa.getHostCard();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
final Game game = host.getGame();
|
final Game game = host.getGame();
|
||||||
@@ -196,7 +198,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -236,6 +238,12 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
|||||||
return Aggregates.random(spells);
|
return Aggregates.random(spells);
|
||||||
} else if (logic.startsWith("Fabricate")) {
|
} else if (logic.startsWith("Fabricate")) {
|
||||||
final int n = Integer.valueOf(logic.substring("Fabricate".length()));
|
final int n = Integer.valueOf(logic.substring("Fabricate".length()));
|
||||||
|
if(spells.size() < 2) {
|
||||||
|
// If the creature is no longer on the battlefield, the option
|
||||||
|
// to add counters is already removed at this point. Return the
|
||||||
|
// only available option: create servo tokens.
|
||||||
|
return spells.get(0);
|
||||||
|
}
|
||||||
SpellAbility counterSA = spells.get(0), tokenSA = spells.get(1);
|
SpellAbility counterSA = spells.get(0), tokenSA = spells.get(1);
|
||||||
|
|
||||||
// check for something which might prevent the counters to be placed on host
|
// check for something which might prevent the counters to be placed on host
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package forge.ai.ability;
|
|||||||
import java.util.List;
|
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.collect.Iterables;
|
||||||
import com.google.common.collect.Lists;
|
import com.google.common.collect.Lists;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
@@ -17,6 +19,7 @@ import forge.game.ability.ApiType;
|
|||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.card.CardPredicates;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
@@ -25,6 +28,7 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
import forge.util.Aggregates;
|
||||||
|
|
||||||
public class ChooseSourceAi extends SpellAbilityAi {
|
public class ChooseSourceAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -52,7 +56,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,9 +137,9 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
|||||||
final Player ai = sa.getActivatingPlayer();
|
final Player ai = sa.getActivatingPlayer();
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
if (!game.getStack().isEmpty()) {
|
if (!game.getStack().isEmpty()) {
|
||||||
Card choseCard = chooseCardOnStack(sa, ai, game);
|
Card chosenCard = chooseCardOnStack(sa, ai, game);
|
||||||
if (choseCard != null) {
|
if (chosenCard != null) {
|
||||||
return choseCard;
|
return chosenCard;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,8 +155,38 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
|||||||
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0;
|
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return ComputerUtilCard.getBestCreatureAI(permanentSources);
|
|
||||||
|
|
||||||
|
// Try to choose the best creature for damage prevention.
|
||||||
|
Card bestCreature = ComputerUtilCard.getBestCreatureAI(permanentSources);
|
||||||
|
if (bestCreature != null) {
|
||||||
|
return bestCreature;
|
||||||
|
} else {
|
||||||
|
// No optimal creature was found above, so try to broaden the choice.
|
||||||
|
if (!Iterables.isEmpty(options)) {
|
||||||
|
List<Card> oppCreatures = CardLists.filter(options, Predicates.and(CardPredicates.Presets.CREATURES,
|
||||||
|
Predicates.not(CardPredicates.isOwner(aiChoser))));
|
||||||
|
List<Card> aiNonCreatures = CardLists.filter(options, Predicates.and(Predicates.not(CardPredicates.Presets.CREATURES),
|
||||||
|
CardPredicates.Presets.PERMANENTS, CardPredicates.isOwner(aiChoser)));
|
||||||
|
|
||||||
|
if (!oppCreatures.isEmpty()) {
|
||||||
|
return ComputerUtilCard.getBestCreatureAI(oppCreatures);
|
||||||
|
} else if (!aiNonCreatures.isEmpty()) {
|
||||||
|
return Aggregates.random(aiNonCreatures);
|
||||||
|
} else {
|
||||||
|
return Aggregates.random(options);
|
||||||
|
}
|
||||||
|
} else if (!game.getStack().isEmpty()) {
|
||||||
|
// No permanent for the AI to choose. Should normally not happen unless using dev mode or something,
|
||||||
|
// but when it does happen, choose the top card on stack if possible (generally it'll be the SA
|
||||||
|
// source) in order to choose at least something, or the game will hang.
|
||||||
|
return game.getStack().peekAbility().getHostCard();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should never get here
|
||||||
|
System.err.println("Unexpected behavior: The AI was unable to choose anything for AF ChooseSource in "
|
||||||
|
+ sa.getHostCard() + ", the game will likely hang.");
|
||||||
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return ComputerUtilCard.getBestAI(options);
|
return ComputerUtilCard.getBestAI(options);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -227,6 +227,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
|||||||
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
|
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (t != null) {
|
||||||
if (t.isCreature())
|
if (t.isCreature())
|
||||||
creatures--;
|
creatures--;
|
||||||
if (t.isPlaneswalker())
|
if (t.isPlaneswalker())
|
||||||
@@ -237,6 +238,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
|||||||
artifacts--;
|
artifacts--;
|
||||||
if (t.isEnchantment())
|
if (t.isEnchantment())
|
||||||
enchantments--;
|
enchantments--;
|
||||||
|
}
|
||||||
|
|
||||||
if (!sa.canTarget(t)) {
|
if (!sa.canTarget(t)) {
|
||||||
list.remove(t);
|
list.remove(t);
|
||||||
|
|||||||
@@ -1,26 +1,23 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
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 forge.ai.*;
|
||||||
import forge.ai.ComputerUtil;
|
import forge.game.Game;
|
||||||
import forge.ai.ComputerUtilCard;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
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.CardPredicates.Presets;
|
import forge.game.card.CardPredicates.Presets;
|
||||||
import forge.game.card.CardUtil;
|
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.player.PlayerActionConfirmMode;
|
||||||
import forge.game.player.PlayerCollection;
|
import forge.game.player.PlayerCollection;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.zone.ZoneType;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class CopyPermanentAi extends SpellAbilityAi {
|
public class CopyPermanentAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
@@ -28,10 +25,21 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
// Card source = sa.getHostCard();
|
// Card source = sa.getHostCard();
|
||||||
// TODO - I'm sure someone can do this AI better
|
// TODO - I'm sure someone can do this AI better
|
||||||
|
|
||||||
|
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
|
||||||
|
String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("MimicVat".equals(aiLogic)) {
|
||||||
|
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
|
||||||
|
} else if ("AtEOT".equals(aiLogic)) {
|
||||||
|
return ph.is(PhaseType.END_OF_TURN);
|
||||||
|
} else if ("AtOppEOT".equals(aiLogic)) {
|
||||||
|
return ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn() != aiPlayer;
|
||||||
|
}
|
||||||
|
|
||||||
if (sa.hasParam("AtEOT") && !aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1)) {
|
if (sa.hasParam("AtEOT") && !aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN1)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -43,6 +51,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (sa.hasParam("Embalm") || sa.hasParam("Eternalize")) {
|
||||||
|
// E.g. Vizier of Many Faces: check to make sure it makes sense to make the token now
|
||||||
|
if (ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa) != AiPlayDecision.WillPlay) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sa.usesTargeting() && sa.hasParam("TargetingPlayer")) {
|
if (sa.usesTargeting() && sa.hasParam("TargetingPlayer")) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
|
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
|
||||||
@@ -55,6 +70,12 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||||
|
final Card host = sa.getHostCard();
|
||||||
|
final Player activator = sa.getActivatingPlayer();
|
||||||
|
final Game game = host.getGame();
|
||||||
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
|
|
||||||
|
|
||||||
// ////
|
// ////
|
||||||
// Targeting
|
// Targeting
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
@@ -69,7 +90,7 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Saheeli Rai + Felidar Guardian combo support
|
// Saheeli Rai + Felidar Guardian combo support
|
||||||
if (sa.getHostCard().getName().equals("Saheeli Rai")) {
|
if ("Saheeli Rai".equals(sourceName)) {
|
||||||
CardCollection felidarGuardian = CardLists.filter(list, CardPredicates.nameEquals("Felidar Guardian"));
|
CardCollection felidarGuardian = CardLists.filter(list, CardPredicates.nameEquals("Felidar Guardian"));
|
||||||
if (felidarGuardian.size() > 0) {
|
if (felidarGuardian.size() > 0) {
|
||||||
// can copy a Felidar Guardian and combo off, so let's do it
|
// can copy a Felidar Guardian and combo off, so let's do it
|
||||||
@@ -119,6 +140,14 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
list.remove(choice);
|
list.remove(choice);
|
||||||
sa.getTargets().add(choice);
|
sa.getTargets().add(choice);
|
||||||
}
|
}
|
||||||
|
} else if (sa.hasParam("Choices")) {
|
||||||
|
// only check for options, does not select there
|
||||||
|
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
|
||||||
|
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host);
|
||||||
|
Collection<Card> betterChoices = getBetterOptions(aiPlayer, sa, choices, !mandatory);
|
||||||
|
if (betterChoices.isEmpty()) {
|
||||||
|
return mandatory;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// if no targeting, it should always be ok
|
// if no targeting, it should always be ok
|
||||||
}
|
}
|
||||||
@@ -141,9 +170,21 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
|
||||||
// Select a card to attach to
|
// Select a card to attach to
|
||||||
|
CardCollection betterOptions = getBetterOptions(ai, sa, options, isOptional);
|
||||||
|
if (!betterOptions.isEmpty()) {
|
||||||
|
options = betterOptions;
|
||||||
|
}
|
||||||
return ComputerUtilCard.getBestAI(options);
|
return ComputerUtilCard.getBestAI(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private CardCollection getBetterOptions(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional) {
|
||||||
|
final Card host = sa.getHostCard();
|
||||||
|
final Player ctrl = host.getController();
|
||||||
|
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
|
||||||
|
// TODO add filter to not select Legendary from Other Player when ai already have a Legendary with that name
|
||||||
|
return CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
|
protected Player chooseSinglePlayer(Player ai, SpellAbility sa, Iterable<Player> options) {
|
||||||
final List<Card> cards = new PlayerCollection(options).getCreaturesInPlay();
|
final List<Card> cards = new PlayerCollection(options).getCreaturesInPlay();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import forge.game.player.PlayerActionConfirmMode;
|
|||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class CopySpellAbilityAi extends SpellAbilityAi {
|
public class CopySpellAbilityAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -36,7 +37,8 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
|
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells,
|
||||||
|
Map<String, Object> params) {
|
||||||
return spells.get(0);
|
return spells.get(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,31 +1,27 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.AiController;
|
import forge.ai.*;
|
||||||
import forge.ai.AiProps;
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
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;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardFactoryUtil;
|
import forge.game.card.CardFactoryUtil;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.cost.CostDiscard;
|
||||||
|
import forge.game.cost.CostExile;
|
||||||
|
import forge.game.cost.CostSacrifice;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
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;
|
||||||
|
|
||||||
|
import java.util.Iterator;
|
||||||
|
|
||||||
public class CounterAi extends SpellAbilityAi {
|
public class CounterAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -44,7 +40,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)) {
|
||||||
@@ -67,6 +63,8 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
// might as well check for player's friendliness
|
// might as well check for player's friendliness
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided
|
||||||
if (sa.hasParam("AITgts") && (topSA.getHostCard() == null
|
if (sa.hasParam("AITgts") && (topSA.getHostCard() == null
|
||||||
|| !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) {
|
|| !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) {
|
||||||
return false;
|
return false;
|
||||||
@@ -76,6 +74,12 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("OppDiscardsHand".equals(sa.getParam("AILogic"))) {
|
||||||
|
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (sa.canTargetSpellAbility(topSA)) {
|
if (sa.canTargetSpellAbility(topSA)) {
|
||||||
sa.getTargets().add(topSA);
|
sa.getTargets().add(topSA);
|
||||||
@@ -94,8 +98,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(ComputerUtil.getOpponentFor(ai), 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")) {
|
||||||
@@ -138,6 +143,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,14 +156,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;
|
||||||
@@ -168,15 +194,38 @@ public class CounterAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// should always counter CMC 1 with Mental Misstep despite a possible limitation by minimum CMC
|
// should not refrain from countering a CMC X spell if that's the only CMC
|
||||||
if (tgtCMC == 1 && "Mental Misstep".equals(source.getName())) {
|
// 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;
|
dontCounter = false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Should ALWAYS counter if it doesn't spend a card, otherwise it wastes an opportunity
|
||||||
|
// to gain card advantage
|
||||||
|
if (sa.isAbility()
|
||||||
|
&& (!sa.getPayCosts().hasSpecificCostType(CostDiscard.class))
|
||||||
|
&& (!sa.getPayCosts().hasSpecificCostType(CostSacrifice.class))
|
||||||
|
&& (!sa.getPayCosts().hasSpecificCostType(CostExile.class))) {
|
||||||
|
// TODO: maybe also disallow CostPayLife?
|
||||||
|
dontCounter = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null Brooch is special - it has a discard cost, but the AI will be
|
||||||
|
// discarding no cards, or is playing a deck where discarding is a benefit
|
||||||
|
// as defined in SpecialCardAi.NullBrooch
|
||||||
|
if (sa.hasParam("AILogic")) {
|
||||||
|
if ("NullBrooch".equals(sa.getParam("AILogic"))) {
|
||||||
|
dontCounter = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (dontCounter) {
|
if (dontCounter) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return toReturn;
|
return toReturn;
|
||||||
}
|
}
|
||||||
@@ -213,8 +262,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(ComputerUtil.getOpponentFor(ai), 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")) {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import forge.game.card.CardCollection;
|
|||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
|
|
||||||
|
|
||||||
@@ -59,7 +60,7 @@ public abstract class CountersAi {
|
|||||||
if (type.equals("M1M1")) {
|
if (type.equals("M1M1")) {
|
||||||
// try to kill the best killable creature, or reduce the best one
|
// try to kill the best killable creature, or reduce the best one
|
||||||
// but try not to target a Undying Creature
|
// but try not to target a Undying Creature
|
||||||
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), "Undying");
|
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING);
|
||||||
if (killable.size() > 0) {
|
if (killable.size() > 0) {
|
||||||
choice = ComputerUtilCard.getBestCreatureAI(killable);
|
choice = ComputerUtilCard.getBestCreatureAI(killable);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
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.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
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;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.card.CardUtil;
|
|
||||||
import forge.game.card.CounterType;
|
|
||||||
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;
|
||||||
@@ -24,6 +17,9 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
import forge.util.collect.FCollection;
|
import forge.util.collect.FCollection;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class CountersMoveAi extends SpellAbilityAi {
|
public class CountersMoveAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||||
@@ -109,6 +105,12 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
if (!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
if (!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
// Make sure that removing the last counter doesn't kill the creature
|
||||||
|
if ("Self".equals(sa.getParam("Source"))) {
|
||||||
|
if (host != null && host.getNetToughness() - 1 <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -188,6 +190,13 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
if (newEval < oldEval) {
|
if (newEval < oldEval) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// check for some specific AI preferences
|
||||||
|
if (src.hasStartOfKeyword("Graft") && "DontMoveCounterIfLethal".equals(src.getSVar("AIGraftPreference"))) {
|
||||||
|
if (cType == CounterType.P1P1 && src.getNetToughness() - src.getTempToughnessBoost() - 1 <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// no target
|
// no target
|
||||||
return true;
|
return true;
|
||||||
@@ -278,7 +287,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
|||||||
// do not steal a P1P1 from Undying if it would die
|
// do not steal a P1P1 from Undying if it would die
|
||||||
// this way
|
// this way
|
||||||
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
|
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
|
||||||
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying") || card.isToken()) {
|
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING) || card.isToken()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,14 +1,9 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
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.ai.*;
|
import forge.ai.*;
|
||||||
import forge.card.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -21,6 +16,7 @@ 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.cost.CostSacrifice;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -33,6 +29,10 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.Aggregates;
|
import forge.util.Aggregates;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class CountersPutAi extends SpellAbilityAi {
|
public class CountersPutAi extends SpellAbilityAi {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -119,15 +119,16 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
|
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
|
||||||
// AI needs to be expanded, since this function can be pretty complex
|
// AI needs to be expanded, since this function can be pretty complex
|
||||||
// based on what the expected targets could be
|
// based on what the expected targets could be
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
final Cost abCost = sa.getPayCosts();
|
final Cost abCost = sa.getPayCosts();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
CardCollection list;
|
CardCollection list;
|
||||||
Card choice = null;
|
Card choice = null;
|
||||||
final String type = sa.getParam("CounterType");
|
final String type = sa.getParam("CounterType");
|
||||||
final String amountStr = sa.getParam("CounterNum");
|
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
|
|
||||||
final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined"))
|
final boolean isClockwork = "True".equals(sa.getParam("UpTo")) && "Self".equals(sa.getParam("Defined"))
|
||||||
&& "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X"))
|
&& "P1P0".equals(sa.getParam("CounterType")) && "Count$xPaid".equals(source.getSVar("X"))
|
||||||
@@ -157,7 +158,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
// receive counters, execpt it has undying
|
// receive counters, execpt it has undying
|
||||||
CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
|
CardCollection oppCreat = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
|
||||||
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1));
|
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterType.M1M1));
|
||||||
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, "Undying");
|
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
|
||||||
|
|
||||||
oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate<Card>() {
|
oppCreatM1 = CardLists.filter(oppCreatM1, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -217,25 +218,70 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
if ("Never".equals(logic)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
} else if ("PayEnergy".equals(logic)) {
|
||||||
|
|
||||||
if ("PayEnergy".equals(sa.getParam("AILogic"))) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
} else if ("PayEnergyConservatively".equals(logic)) {
|
||||||
|
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 ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) {
|
|
||||||
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;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (logic.equals("MarkOppCreature")) {
|
||||||
|
if (!ph.is(PhaseType.END_OF_TURN)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
CardCollection oppCreats = CardLists.filter(ai.getOpponents().getCreaturesInPlay(),
|
||||||
|
Predicates.and(Predicates.not(CardPredicates.hasCounter(CounterType.getType(type))),
|
||||||
|
CardPredicates.isTargetableBy(sa)));
|
||||||
|
|
||||||
|
if (!oppCreats.isEmpty()) {
|
||||||
|
Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
|
||||||
|
sa.resetTargets();
|
||||||
|
sa.getTargets().add(bestCreat);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else if (logic.equals("CheckDFC")) {
|
||||||
|
// for cards like Ludevic's Test Subject
|
||||||
|
if (!source.canTransform()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
|
if (sa.getConditions() != null && !sa.getConditions().areMet(sa) && sa.getSubAbility() == null) {
|
||||||
@@ -266,7 +312,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
// TODO handle proper calculation of X values based on Cost
|
// TODO handle proper calculation of X values based on Cost
|
||||||
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
int amount = AbilityUtils.calculateAmount(source, amountStr, sa);
|
||||||
|
|
||||||
if ("Fight".equals(sa.getParam("AILogic"))) {
|
if ("Fight".equals(logic)) {
|
||||||
int nPump = 0;
|
int nPump = 0;
|
||||||
if (type.equals("P1P1")) {
|
if (type.equals("P1P1")) {
|
||||||
nPump = amount;
|
nPump = amount;
|
||||||
@@ -274,7 +320,8 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
return FightAi.canFightAi(ai, sa, nPump, nPump);
|
return FightAi.canFightAi(ai, sa, nPump, nPump);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (amountStr.equals("X") && source.getSVar(amountStr).equals("Count$xPaid")) {
|
if (amountStr.equals("X")) {
|
||||||
|
if (source.getSVar(amountStr).equals("Count$xPaid")) {
|
||||||
// By default, set PayX here to maximum value (used for most SAs of this type).
|
// By default, set PayX here to maximum value (used for most SAs of this type).
|
||||||
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
amount = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||||
|
|
||||||
@@ -290,10 +337,19 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
amount = Math.min(amount, maxCtrs - curCtrs);
|
amount = Math.min(amount, maxCtrs - curCtrs);
|
||||||
if (amount <= 0) { return false; }
|
if (amount <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
source.setSVar("PayX", Integer.toString(amount));
|
source.setSVar("PayX", Integer.toString(amount));
|
||||||
|
} else if ("ExiledCreatureFromGraveCMC".equals(logic)) {
|
||||||
|
// e.g. Necropolis
|
||||||
|
amount = Aggregates.max(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES), CardPredicates.Accessors.fnGetCmc);
|
||||||
|
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't use it if no counters to add
|
// don't use it if no counters to add
|
||||||
@@ -301,16 +357,10 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ("Polukranos".equals(sa.getParam("AILogic"))) {
|
if ("Polukranos".equals(logic)) {
|
||||||
|
|
||||||
CardCollection humCreatures = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
|
CardCollection targets = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
|
||||||
|
|
||||||
final CardCollection targets = CardLists.filter(humCreatures, new Predicate<Card>() {
|
|
||||||
@Override
|
|
||||||
public boolean apply(final Card c) {
|
|
||||||
return !(c.hasProtectionFrom(source) || c.hasKeyword("Shroud") || c.hasKeyword("Hexproof"));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!targets.isEmpty()){
|
if (!targets.isEmpty()){
|
||||||
boolean canSurvive = false;
|
boolean canSurvive = false;
|
||||||
for (Card humanCreature : targets) {
|
for (Card humanCreature : targets) {
|
||||||
@@ -324,10 +374,8 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
if ("AlwaysAtOppEOT".equals(logic)) {
|
||||||
|
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
|
||||||
if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) {
|
|
||||||
if (ph.is(PhaseType.END_OF_TURN) && !ph.isPlayerTurn(ai)) {
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -476,7 +524,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||||
// Don't activate Curse abilities on my cards and non-curse abilites
|
// Don't activate Curse abilities on my cards and non-curse abilities
|
||||||
// on my opponents
|
// on my opponents
|
||||||
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
|
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
|
||||||
return false;
|
return false;
|
||||||
@@ -486,7 +534,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
// each non +1/+1 counter on the card is a 10% chance of not
|
// each non +1/+1 counter on the card is a 10% chance of not
|
||||||
// activating this ability.
|
// activating this ability.
|
||||||
|
|
||||||
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (r.nextFloat() < (.1 * currCounters))) {
|
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// Instant +1/+1
|
// Instant +1/+1
|
||||||
@@ -499,7 +547,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -530,13 +578,14 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
Card choice = null;
|
Card choice = null;
|
||||||
final String type = sa.getParam("CounterType");
|
final String type = sa.getParam("CounterType");
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
final String amountStr = sa.getParam("CounterNum");
|
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||||
|
|
||||||
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|
||||||
|| (sa.getRootAbility().isTrigger() || !sa.getRootAbility().isOptionalTrigger());
|
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
|
||||||
|
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
CardCollection list = null;
|
CardCollection list = null;
|
||||||
@@ -575,7 +624,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
SpellAbility animate = sa.findSubAbilityByType(ApiType.Animate);
|
SpellAbility animate = sa.findSubAbilityByType(ApiType.Animate);
|
||||||
if (!lands.isEmpty() && animate != null) {
|
if (!lands.isEmpty() && animate != null) {
|
||||||
choice = ComputerUtilCard.getWorstLand(lands);
|
choice = ComputerUtilCard.getWorstLand(lands);
|
||||||
} else if ("BoonCounterOnOppCreature".equals(sa.getParam("AILogic"))) {
|
} else if ("BoonCounterOnOppCreature".equals(logic)) {
|
||||||
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
choice = ComputerUtilCard.getWorstCreatureAI(list);
|
||||||
} else {
|
} else {
|
||||||
choice = CountersAi.chooseBoonTarget(list, type);
|
choice = CountersAi.chooseBoonTarget(list, type);
|
||||||
@@ -611,7 +660,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
boolean preferred = true;
|
boolean preferred = true;
|
||||||
CardCollection list;
|
CardCollection list;
|
||||||
final String type = sa.getParam("CounterType");
|
final String type = sa.getParam("CounterType");
|
||||||
final String amountStr = sa.getParam("CounterNum");
|
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||||
int left = amount;
|
int left = amount;
|
||||||
@@ -678,6 +727,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()) {
|
||||||
@@ -747,14 +803,15 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
if (mode == PlayerActionConfirmMode.Tribute) {
|
if (mode == PlayerActionConfirmMode.Tribute) {
|
||||||
// add counter if that opponent has a giant creature
|
// add counter if that opponent has a giant creature
|
||||||
final List<Card> creats = player.getCreaturesInPlay();
|
final List<Card> creats = player.getCreaturesInPlay();
|
||||||
final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CounterNum"), sa);
|
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||||
|
final int tributeAmount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||||
|
|
||||||
final boolean isHaste = source.hasKeyword("Haste");
|
final boolean isHaste = source.hasKeyword(Keyword.HASTE);
|
||||||
List<Card> threatening = CardLists.filter(creats, new Predicate<Card>() {
|
List<Card> threatening = CardLists.filter(creats, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Card c) {
|
public boolean apply(Card c) {
|
||||||
return CombatUtil.canBlock(source, c, !isHaste)
|
return CombatUtil.canBlock(source, c, !isHaste)
|
||||||
&& (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword("DeathTouch"));
|
&& (c.getNetToughness() > source.getNetPower() + tributeAmount || c.hasKeyword(Keyword.DEATHTOUCH));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!threatening.isEmpty()) {
|
if (!threatening.isEmpty()) {
|
||||||
@@ -771,7 +828,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
List<Card> canBlock = CardLists.filter(creats, new Predicate<Card>() {
|
List<Card> canBlock = CardLists.filter(creats, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Card c) {
|
public boolean apply(Card c) {
|
||||||
return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword("DeathTouch"));
|
return CombatUtil.canBlock(source, c) && (c.getNetToughness() > source.getNetPower() || c.hasKeyword(Keyword.DEATHTOUCH));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (!canBlock.isEmpty()) {
|
if (!canBlock.isEmpty()) {
|
||||||
@@ -806,7 +863,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final CounterType type = CounterType.valueOf(sa.getParam("CounterType"));
|
final CounterType type = CounterType.valueOf(sa.getParam("CounterType"));
|
||||||
final String amountStr = sa.getParam("CounterNum");
|
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
|
||||||
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||||
|
|
||||||
final boolean isCurse = sa.isCurse();
|
final boolean isCurse = sa.isCurse();
|
||||||
@@ -864,7 +921,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
final CardCollection persist = CardLists.filter(filtered, new Predicate<Card>() {
|
final CardCollection persist = CardLists.filter(filtered, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Card input) {
|
public boolean apply(Card input) {
|
||||||
if (!input.hasKeyword("Persist"))
|
if (!input.hasKeyword(Keyword.PERSIST))
|
||||||
return false;
|
return false;
|
||||||
return input.getCounters(CounterType.M1M1) <= amount;
|
return input.getCounters(CounterType.M1M1) <= amount;
|
||||||
}
|
}
|
||||||
@@ -877,7 +934,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
final CardCollection undying = CardLists.filter(filtered, new Predicate<Card>() {
|
final CardCollection undying = CardLists.filter(filtered, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Card input) {
|
public boolean apply(Card input) {
|
||||||
if (!input.hasKeyword("Undying"))
|
if (!input.hasKeyword(Keyword.UNDYING))
|
||||||
return false;
|
return false;
|
||||||
return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount;
|
return input.getCounters(CounterType.P1P1) <= amount && input.getNetToughness() > amount;
|
||||||
}
|
}
|
||||||
@@ -902,7 +959,7 @@ public class CountersPutAi extends SpellAbilityAi {
|
|||||||
if (e instanceof Card) {
|
if (e instanceof Card) {
|
||||||
Card c = (Card) e;
|
Card c = (Card) e;
|
||||||
if (c.getController().isOpponentOf(ai)) {
|
if (c.getController().isOpponentOf(ai)) {
|
||||||
if (options.contains(CounterType.M1M1) && !c.hasKeyword("Undying")) {
|
if (options.contains(CounterType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
|
||||||
return CounterType.M1M1;
|
return CounterType.M1M1;
|
||||||
}
|
}
|
||||||
for (CounterType type : options) {
|
for (CounterType type : options) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
import com.google.common.collect.Lists;
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
@@ -21,7 +21,6 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class CountersPutAllAi extends SpellAbilityAi {
|
public class CountersPutAllAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
@@ -29,7 +28,6 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
// AI needs to be expanded, since this function can be pretty complex
|
// AI needs to be expanded, since this function can be pretty complex
|
||||||
// based on what
|
// based on what
|
||||||
// the expected targets could be
|
// the expected targets could be
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
final Cost abCost = sa.getPayCosts();
|
final Cost abCost = sa.getPayCosts();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
List<Card> hList;
|
List<Card> hList;
|
||||||
@@ -54,7 +52,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) {
|
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -63,6 +61,10 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (logic.equals("AtOppEOT")) {
|
||||||
|
if (!(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
@@ -85,7 +87,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
boolean chance = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
if (curse) {
|
if (curse) {
|
||||||
if (type.equals("M1M1")) {
|
if (type.equals("M1M1")) {
|
||||||
@@ -135,7 +137,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
return chance;
|
return chance;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((r.nextFloat() < .6667) && chance);
|
return ((MyRandom.getRandom().nextFloat() < .6667) && chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -149,4 +151,31 @@ public class CountersPutAllAi extends SpellAbilityAi {
|
|||||||
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() >= ComputerUtil.getOpponentFor(player).getCreaturesInPlay().size();
|
return player.getCreaturesInPlay().size() >= ComputerUtil.getOpponentFor(player).getCreaturesInPlay().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||||
|
if (sa.usesTargeting()) {
|
||||||
|
List<Player> players = Lists.newArrayList();
|
||||||
|
if (!sa.isCurse()) {
|
||||||
|
players.add(aiPlayer);
|
||||||
|
}
|
||||||
|
players.addAll(aiPlayer.getOpponents());
|
||||||
|
players.addAll(aiPlayer.getAllies());
|
||||||
|
if (sa.isCurse()) {
|
||||||
|
players.add(aiPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final Player p : players) {
|
||||||
|
if (p.canBeTargetedBy(sa) && sa.canTarget(p)) {
|
||||||
|
boolean preferred = false;
|
||||||
|
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
|
||||||
|
sa.resetTargets();
|
||||||
|
sa.getTargets().add(p);
|
||||||
|
return preferred || mandatory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mandatory;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,25 +17,21 @@
|
|||||||
*/
|
*/
|
||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
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;
|
||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.*;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.card.CardCollectionView;
|
|
||||||
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.PlayerController.BinaryChoiceType;
|
import forge.game.player.PlayerController.BinaryChoiceType;
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* AbilityFactory_PutOrRemoveCountersAi class.
|
* AbilityFactory_PutOrRemoveCountersAi class.
|
||||||
@@ -61,7 +57,6 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
|
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final Card source = sa.getHostCard();
|
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
|
|
||||||
final int amount = Integer.valueOf(sa.getParam("CounterNum"));
|
final int amount = Integer.valueOf(sa.getParam("CounterNum"));
|
||||||
@@ -75,13 +70,8 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.hasParam("AITgts")) {
|
// Filter AI-specific targets if provided
|
||||||
String aiTgts = sa.getParam("AITgts");
|
list = ComputerUtil.filterAITgts(sa, ai, list, false);
|
||||||
CardCollection prefList = CardLists.getValidCards(list, aiTgts.split(","), ai, source, sa);
|
|
||||||
if (!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
|
||||||
list = prefList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sa.hasParam("CounterType")) {
|
if (sa.hasParam("CounterType")) {
|
||||||
// currently only Jhoira's Timebug
|
// currently only Jhoira's Timebug
|
||||||
@@ -122,7 +112,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
|||||||
// with one touch
|
// with one touch
|
||||||
CardCollection planeswalkerList = CardLists.filter(
|
CardCollection planeswalkerList = CardLists.filter(
|
||||||
CardLists.filterControlledBy(countersList, ai.getOpponents()),
|
CardLists.filterControlledBy(countersList, ai.getOpponents()),
|
||||||
CardPredicates.Presets.PLANEWALKERS,
|
CardPredicates.Presets.PLANESWALKERS,
|
||||||
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
|
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
|
||||||
|
|
||||||
if (!planeswalkerList.isEmpty()) {
|
if (!planeswalkerList.isEmpty()) {
|
||||||
@@ -135,7 +125,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
|
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
|
||||||
|
|
||||||
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist");
|
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
|
||||||
if (!aiPersistList.isEmpty()) {
|
if (!aiPersistList.isEmpty()) {
|
||||||
aiM1M1List = aiPersistList;
|
aiM1M1List = aiPersistList;
|
||||||
}
|
}
|
||||||
@@ -147,7 +137,7 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// do as P1P1 part
|
// do as P1P1 part
|
||||||
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
|
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
|
||||||
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying");
|
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
|
||||||
|
|
||||||
if (!aiUndyingList.isEmpty()) {
|
if (!aiUndyingList.isEmpty()) {
|
||||||
aiP1P1List = aiUndyingList;
|
aiP1P1List = aiUndyingList;
|
||||||
@@ -236,9 +226,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
|||||||
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
|
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
|
||||||
return CounterType.ICE;
|
return CounterType.ICE;
|
||||||
}
|
}
|
||||||
} else if (tgt.hasKeyword("Undying") && options.contains(CounterType.P1P1)) {
|
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.P1P1)) {
|
||||||
return CounterType.P1P1;
|
return CounterType.P1P1;
|
||||||
} else if (tgt.hasKeyword("Persist") && options.contains(CounterType.M1M1)) {
|
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.M1M1)) {
|
||||||
return CounterType.M1M1;
|
return CounterType.M1M1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,9 +272,9 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
|
|||||||
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
|
if (!ai.isCardInPlay("Marit Lage") || noLegendary) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (type.equals(CounterType.M1M1) && tgt.hasKeyword("Persist")) {
|
} else if (type.equals(CounterType.M1M1) && tgt.hasKeyword(Keyword.PERSIST)) {
|
||||||
return false;
|
return false;
|
||||||
} else if (type.equals(CounterType.P1P1) && tgt.hasKeyword("Undying")) {
|
} else if (type.equals(CounterType.P1P1) && tgt.hasKeyword(Keyword.UNDYING)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
import com.google.common.base.Predicates;
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
@@ -10,12 +8,8 @@ import forge.ai.SpellAbilityAi;
|
|||||||
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.keyword.Keyword;
|
||||||
import forge.game.card.CardCollectionView;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.card.CardPredicates;
|
|
||||||
import forge.game.card.CounterType;
|
|
||||||
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;
|
||||||
@@ -23,6 +17,9 @@ 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;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class CountersRemoveAi extends SpellAbilityAi {
|
public class CountersRemoveAi extends SpellAbilityAi {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -102,13 +99,8 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.hasParam("AITgts")) {
|
// Filter AI-specific targets if provided
|
||||||
String aiTgts = sa.getParam("AITgts");
|
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, false);
|
||||||
CardCollection prefList = CardLists.getValidCards(list, aiTgts.split(","), ai, source, sa);
|
|
||||||
if (!prefList.isEmpty() || sa.hasParam("AITgtsStrict")) {
|
|
||||||
list = prefList;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
|
boolean noLegendary = game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLegendRule);
|
||||||
|
|
||||||
@@ -130,7 +122,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
||||||
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
|
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
|
||||||
|
|
||||||
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANEWALKERS,
|
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANESWALKERS,
|
||||||
CardPredicates.hasCounter(CounterType.LOYALTY, 5));
|
CardPredicates.hasCounter(CounterType.LOYALTY, 5));
|
||||||
|
|
||||||
if (!planeswalkerList.isEmpty()) {
|
if (!planeswalkerList.isEmpty()) {
|
||||||
@@ -174,10 +166,11 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get rid of Planeswalkers:
|
// Get rid of Planeswalkers:
|
||||||
list = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
|
list = game.getPlayers().getCardsIn(ZoneType.Battlefield);
|
||||||
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
|
list = CardLists.filter(list, CardPredicates.isTargetableBy(sa));
|
||||||
|
|
||||||
CardCollection planeswalkerList = CardLists.filter(list, CardPredicates.Presets.PLANEWALKERS,
|
CardCollection planeswalkerList = CardLists.filter(list,
|
||||||
|
Predicates.and(CardPredicates.Presets.PLANESWALKERS, CardPredicates.isControlledByAnyOf(ai.getOpponents())),
|
||||||
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
|
CardPredicates.hasLessCounter(CounterType.LOYALTY, amount));
|
||||||
|
|
||||||
if (!planeswalkerList.isEmpty()) {
|
if (!planeswalkerList.isEmpty()) {
|
||||||
@@ -196,7 +189,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
|
CardCollection aiM1M1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1));
|
||||||
|
|
||||||
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, "Persist");
|
CardCollection aiPersistList = CardLists.getKeyword(aiM1M1List, Keyword.PERSIST);
|
||||||
if (!aiPersistList.isEmpty()) {
|
if (!aiPersistList.isEmpty()) {
|
||||||
aiM1M1List = aiPersistList;
|
aiM1M1List = aiPersistList;
|
||||||
}
|
}
|
||||||
@@ -208,7 +201,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// do as P1P1 part
|
// do as P1P1 part
|
||||||
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
|
CardCollection aiP1P1List = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.P1P1));
|
||||||
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, "Undying");
|
CardCollection aiUndyingList = CardLists.getKeyword(aiM1M1List, Keyword.UNDYING);
|
||||||
|
|
||||||
if (!aiUndyingList.isEmpty()) {
|
if (!aiUndyingList.isEmpty()) {
|
||||||
aiP1P1List = aiUndyingList;
|
aiP1P1List = aiUndyingList;
|
||||||
@@ -238,7 +231,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
CardCollection aiList = CardLists.filterControlledBy(list, ai);
|
CardCollection aiList = CardLists.filterControlledBy(list, ai);
|
||||||
aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount));
|
aiList = CardLists.filter(aiList, CardPredicates.hasCounter(CounterType.M1M1, amount));
|
||||||
|
|
||||||
CardCollection aiPersist = CardLists.getKeyword(aiList, "Persist");
|
CardCollection aiPersist = CardLists.getKeyword(aiList, Keyword.PERSIST);
|
||||||
if (!aiPersist.isEmpty()) {
|
if (!aiPersist.isEmpty()) {
|
||||||
aiList = aiPersist;
|
aiList = aiPersist;
|
||||||
}
|
}
|
||||||
@@ -261,7 +254,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
// targeting ai creatures too
|
// targeting ai creatures too
|
||||||
CardCollection aiList = CardLists.filterControlledBy(list, ai);
|
CardCollection aiList = CardLists.filterControlledBy(list, ai);
|
||||||
if (!aiList.isEmpty()) {
|
if (!aiList.isEmpty()) {
|
||||||
CardCollection aiListUndying = CardLists.getKeyword(aiList, "Undying");
|
CardCollection aiListUndying = CardLists.getKeyword(aiList, Keyword.UNDYING);
|
||||||
if (!aiListUndying.isEmpty()) {
|
if (!aiListUndying.isEmpty()) {
|
||||||
aiList = aiListUndying;
|
aiList = aiListUndying;
|
||||||
}
|
}
|
||||||
@@ -274,7 +267,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
// need to target opponent creatures
|
// need to target opponent creatures
|
||||||
CardCollection oppList = CardLists.filterControlledBy(list, ai.getOpponents());
|
CardCollection oppList = CardLists.filterControlledBy(list, ai.getOpponents());
|
||||||
if (!oppList.isEmpty()) {
|
if (!oppList.isEmpty()) {
|
||||||
CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, "Undying");
|
CardCollection oppListNotUndying = CardLists.getNotKeyword(oppList, Keyword.UNDYING);
|
||||||
if (!oppListNotUndying.isEmpty()) {
|
if (!oppListNotUndying.isEmpty()) {
|
||||||
oppList = oppListNotUndying;
|
oppList = oppListNotUndying;
|
||||||
}
|
}
|
||||||
@@ -366,9 +359,9 @@ public class CountersRemoveAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (options.contains(CounterType.M1M1) && target.hasKeyword("Persist")) {
|
if (options.contains(CounterType.M1M1) && target.hasKeyword(Keyword.PERSIST)) {
|
||||||
return CounterType.M1M1;
|
return CounterType.M1M1;
|
||||||
} else if (options.contains(CounterType.P1P1) && target.hasKeyword("Undying")) {
|
} else if (options.contains(CounterType.P1P1) && target.hasKeyword(Keyword.UNDYING)) {
|
||||||
return CounterType.M1M1;
|
return CounterType.M1M1;
|
||||||
}
|
}
|
||||||
for (CounterType type : options) {
|
for (CounterType type : options) {
|
||||||
|
|||||||
@@ -1,25 +1,71 @@
|
|||||||
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.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.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardPredicates;
|
import forge.game.card.CardPredicates;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.trigger.Trigger;
|
||||||
|
import forge.game.trigger.TriggerDamageDone;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
public abstract class DamageAiBase extends SpellAbilityAi {
|
public abstract class DamageAiBase extends SpellAbilityAi {
|
||||||
|
protected boolean avoidTargetP(final Player comp, final SpellAbility sa) {
|
||||||
|
Player enemy = ComputerUtil.getOpponentFor(comp);
|
||||||
|
// Logic for cards that damage owner, like Fireslinger
|
||||||
|
// Do not target a player if they aren't below 75% of our health.
|
||||||
|
// Unless Lifelink will cancel the damage to us
|
||||||
|
Card hostcard = sa.getHostCard();
|
||||||
|
boolean lifelink = hostcard.hasKeyword(Keyword.LIFELINK);
|
||||||
|
if (!lifelink) {
|
||||||
|
for (Card ench : hostcard.getEnchantedBy(false)) {
|
||||||
|
// Treat cards enchanted by older cards with "when enchanted creature deals damage, gain life" as if they had lifelink.
|
||||||
|
if (ench.hasSVar("LikeLifeLink")) {
|
||||||
|
if ("True".equals(ench.getSVar("LikeLifeLink"))) {
|
||||||
|
lifelink = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ("SelfDamage".equals(sa.getParam("AILogic"))) {
|
||||||
|
if (comp.getLife() * 0.75 < enemy.getLife()) {
|
||||||
|
if (!lifelink) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
|
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
|
||||||
|
// TODO: once the "planeswalker redirection" rule is removed completely, just remove this code and
|
||||||
|
// remove the "burn Planeswalkers" code in the called method.
|
||||||
|
return shouldTgtP(comp, sa, d, noPrevention, false);
|
||||||
|
}
|
||||||
|
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention, final boolean noPlaneswalkerRedirection) {
|
||||||
int restDamage = d;
|
int restDamage = d;
|
||||||
final Game game = comp.getGame();
|
final Game game = comp.getGame();
|
||||||
final Player enemy = ComputerUtil.getOpponentFor(comp);
|
Player enemy = ComputerUtil.getOpponentFor(comp);
|
||||||
|
boolean dmgByCardsInHand = false;
|
||||||
|
|
||||||
|
if ("X".equals(sa.getParam("NumDmg")) && sa.getHostCard() != null && sa.hasSVar(sa.getParam("NumDmg")) &&
|
||||||
|
sa.getHostCard().getSVar(sa.getParam("NumDmg")).equals("TargetedPlayer$CardsInHand")) {
|
||||||
|
dmgByCardsInHand = true;
|
||||||
|
}
|
||||||
|
// Not sure if type choice implemented for the AI yet but it should at least recognize this spell hits harder on larger enemy hand size
|
||||||
|
if ("Blood Oath".equals(sa.getHostCard().getName())) {
|
||||||
|
dmgByCardsInHand = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!sa.canTarget(enemy)) {
|
if (!sa.canTarget(enemy)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -27,10 +73,28 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// burn Planeswalkers
|
// Benefits hitting players?
|
||||||
if (Iterables.any(enemy.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANEWALKERS)) {
|
// If has triggered ability on dealing damage to an opponent, go for it!
|
||||||
|
Card hostcard = sa.getHostCard();
|
||||||
|
for (Trigger trig : hostcard.getTriggers()) {
|
||||||
|
if (trig instanceof TriggerDamageDone) {
|
||||||
|
if (("Opponent".equals(trig.getParam("ValidTarget")))
|
||||||
|
&& (!"True".equals(trig.getParam("CombatDamage")))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// burn Planeswalkers
|
||||||
|
// TODO: Must be removed completely when the "planeswalker redirection" rule is removed.
|
||||||
|
if (!noPlaneswalkerRedirection
|
||||||
|
&& Iterables.any(enemy.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANESWALKERS)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (avoidTargetP(comp, sa)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!noPrevention) {
|
if (!noPrevention) {
|
||||||
restDamage = ComputerUtilCombat.predictDamageTo(enemy, restDamage, sa.getHostCard(), false);
|
restDamage = ComputerUtilCombat.predictDamageTo(enemy, restDamage, sa.getHostCard(), false);
|
||||||
@@ -71,10 +135,20 @@ 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 Sudden Impact type spell, and can hit at least 3 cards during draw phase
|
||||||
|
// have a 100% chance to go for it, enemy hand will only lose cards over time!
|
||||||
|
// But if 3 or less cards, use normal rules, just in case enemy starts holding card or plays a draw spell or we need mana for other instants.
|
||||||
|
if (phase.isPlayerTurn(enemy)) {
|
||||||
|
if (dmgByCardsInHand
|
||||||
|
&& (phase.is(PhaseType.DRAW))
|
||||||
|
&& (enemy.getCardsIn(ZoneType.Hand).size() > 3)) {
|
||||||
|
value = 1;
|
||||||
|
} else if (phase.is(PhaseType.END_OF_TURN)
|
||||||
|
|| ((dmgByCardsInHand && phase.getPhase().isAfter(PhaseType.UPKEEP)))) {
|
||||||
value = 1.5f * restDamage / enemy.getLife();
|
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
|
||||||
for (int i = 3; i < hand.size(); i++) {
|
for (int i = 3; i < hand.size(); i++) {
|
||||||
value *= 1.1f;
|
value *= 1.1f;
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -9,6 +8,7 @@ import forge.game.card.CardCollection;
|
|||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -16,8 +16,6 @@ import forge.game.spellability.TargetRestrictions;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class DamageAllAi extends SpellAbilityAi {
|
public class DamageAllAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
@@ -26,8 +24,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
final Random r = MyRandom.getRandom();
|
if (MyRandom.getRandom().nextFloat() > Math.pow(.9, sa.getActivationsThisTurn())) {
|
||||||
if (r.nextFloat() > Math.pow(.9, sa.getActivationsThisTurn())) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// abCost stuff that should probably be centralized...
|
// abCost stuff that should probably be centralized...
|
||||||
@@ -56,9 +53,26 @@ 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;
|
||||||
|
Player bestOpp = determineOppToKill(ai, sa, source, x);
|
||||||
|
if (bestOpp != null) {
|
||||||
|
// we can finish off a player, so go for it
|
||||||
|
|
||||||
|
// TODO: improve this by possibly damaging more creatures
|
||||||
|
// on the battlefield belonging to other opponents at the same
|
||||||
|
// time, if viable
|
||||||
|
best_x = bestOpp.getLife();
|
||||||
|
} else {
|
||||||
|
// see if it's possible to get value from killing off creatures
|
||||||
for (int i = 0; i <= x; i++) {
|
for (int i = 0; i <= x; i++) {
|
||||||
final int value = evaluateDamageAll(ai, sa, source, i);
|
final int value = evaluateDamageAll(ai, sa, source, i);
|
||||||
if (value > best) {
|
if (value > best) {
|
||||||
@@ -66,6 +80,8 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
best_x = i;
|
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 +95,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 = ComputerUtil.getOpponentFor(ai);
|
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,12 +137,6 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// if we can kill human, do it
|
|
||||||
if ((validP.equals("Player") || validP.contains("Opponent"))
|
|
||||||
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
int minGain = 200; // The minimum gain in destroyed creatures
|
int minGain = 200; // The minimum gain in destroyed creatures
|
||||||
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
|
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
|
||||||
if (computerList.isEmpty()) {
|
if (computerList.isEmpty()) {
|
||||||
@@ -236,7 +269,7 @@ public class DamageAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
list = CardLists.getNotKeyword(list, "Indestructible");
|
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
|
||||||
list = CardLists.filter(list, filterKillable);
|
list = CardLists.filter(list, filterKillable);
|
||||||
|
|
||||||
return list;
|
return list;
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
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.cost.CostRemoveCounter;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -28,6 +22,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 +95,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 +125,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);
|
||||||
@@ -131,6 +162,18 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if ("NinThePainArtist".equals(logic)) {
|
||||||
|
// Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard
|
||||||
|
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
|
||||||
|
boolean doTarget = damageTargetAI(ai, sa, dmg, true);
|
||||||
|
if (doTarget) {
|
||||||
|
Card tgt = sa.getTargets().getFirstTargetedCard();
|
||||||
|
if (tgt != null) {
|
||||||
|
return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sourceName.equals("Sorin, Grim Nemesis")) {
|
if (sourceName.equals("Sorin, Grim Nemesis")) {
|
||||||
@@ -157,7 +200,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -230,35 +273,21 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
final Player activator = sa.getActivatingPlayer();
|
final Player activator = sa.getActivatingPlayer();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
final Game game = source.getGame();
|
final Game game = source.getGame();
|
||||||
List<Card> hPlay = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), activator, source, sa);
|
List<Card> hPlay = getTargetableCards(ai, sa, pl, tgt, activator, source, game);
|
||||||
|
|
||||||
if (activator.equals(ai)) {
|
List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||||
hPlay = CardLists.filterControlledBy(hPlay, pl);
|
|
||||||
}
|
|
||||||
|
|
||||||
final List<GameObject> objects = Lists.newArrayList(sa.getTargets().getTargets());
|
|
||||||
if (sa.hasParam("TargetUnique")) {
|
|
||||||
objects.addAll(sa.getUniqueTargets());
|
|
||||||
}
|
|
||||||
for (final Object o : objects) {
|
|
||||||
if (o instanceof Card) {
|
|
||||||
final Card c = (Card) o;
|
|
||||||
if (hPlay.contains(c)) {
|
|
||||||
hPlay.remove(c);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
hPlay = CardLists.getTargetableCards(hPlay, sa);
|
|
||||||
|
|
||||||
final List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return c.getSVar("Targeting").equals("Dies")
|
return c.getSVar("Targeting").equals("Dies")
|
||||||
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d) && !ComputerUtil.canRegenerate(ai, c)
|
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d)
|
||||||
|
&& !ComputerUtil.canRegenerate(ai, c)
|
||||||
&& !(c.getSVar("SacMe").length() > 0);
|
&& !(c.getSVar("SacMe").length() > 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Filter AI-specific targets if provided
|
||||||
|
killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true);
|
||||||
|
|
||||||
Card targetCard = null;
|
Card targetCard = null;
|
||||||
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
|
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
|
||||||
if (sa.getTargetRestrictions().canTgtPlaneswalker()) {
|
if (sa.getTargetRestrictions().canTgtPlaneswalker()) {
|
||||||
@@ -293,6 +322,114 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* dealDamageChooseTgtPW.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param d
|
||||||
|
* a int.
|
||||||
|
* @param noPrevention
|
||||||
|
* a boolean.
|
||||||
|
* @param pl
|
||||||
|
* a {@link forge.game.player.Player} object.
|
||||||
|
* @param mandatory
|
||||||
|
* a boolean.
|
||||||
|
* @return a {@link forge.game.card.Card} object.
|
||||||
|
*/
|
||||||
|
private Card dealDamageChooseTgtPW(final Player ai, final SpellAbility sa, final int d, final boolean noPrevention,
|
||||||
|
final Player pl, final boolean mandatory) {
|
||||||
|
|
||||||
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
final Player activator = sa.getActivatingPlayer();
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
final Game game = source.getGame();
|
||||||
|
List<Card> hPlay = CardLists.filter(getTargetableCards(ai, sa, pl, tgt, activator, source, game), CardPredicates.Presets.PLANESWALKERS);
|
||||||
|
|
||||||
|
List<Card> killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(final Card c) {
|
||||||
|
return c.getSVar("Targeting").equals("Dies")
|
||||||
|
|| (ComputerUtilCombat.getEnoughDamageToKill(c, d, source, false, noPrevention) <= d)
|
||||||
|
&& !ComputerUtil.canRegenerate(ai, c)
|
||||||
|
&& !(c.getSVar("SacMe").length() > 0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter AI-specific targets if provided
|
||||||
|
killables = ComputerUtil.filterAITgts(sa, ai, new CardCollection(killables), true);
|
||||||
|
|
||||||
|
// We can kill a planeswalker, so go for it
|
||||||
|
if (pl.isOpponentOf(ai) && activator.equals(ai) && !killables.isEmpty()) {
|
||||||
|
return ComputerUtilCard.getBestPlaneswalkerAI(killables);
|
||||||
|
}
|
||||||
|
|
||||||
|
// We can hurt a planeswalker, so rank the one which is the best target
|
||||||
|
if (!hPlay.isEmpty() && pl.isOpponentOf(ai) && activator.equals(ai)) {
|
||||||
|
return getBestPlaneswalkerToDamage(hPlay);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Card getBestPlaneswalkerToDamage(final List<Card> pws) {
|
||||||
|
Card bestTgt = null;
|
||||||
|
|
||||||
|
// As of right now, ranks planeswalkers by their Current Loyalty * 10 + Big buff if close to "Ultimate"
|
||||||
|
int bestScore = 0;
|
||||||
|
for (Card pw : pws) {
|
||||||
|
int curLoyalty = pw.getCounters(CounterType.LOYALTY);
|
||||||
|
int pwScore = curLoyalty * 10;
|
||||||
|
|
||||||
|
for (SpellAbility sa : pw.getSpellAbilities()) {
|
||||||
|
if (sa.hasParam("Ultimate") && sa.getPayCosts() != null) {
|
||||||
|
int loyaltyCost = 0;
|
||||||
|
CostRemoveCounter remLoyalty = sa.getPayCosts().getCostPartByType(CostRemoveCounter.class);
|
||||||
|
if (remLoyalty != null) {
|
||||||
|
// if remLoyalty is null, generally there's an AddCounter<0/LOYALTY> cost, like for Gideon Jura.
|
||||||
|
loyaltyCost = remLoyalty.convertAmount();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
|
||||||
|
// Will ultimate soon
|
||||||
|
pwScore += 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pwScore > bestScore) {
|
||||||
|
bestScore = pwScore;
|
||||||
|
bestTgt = pw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return bestTgt;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private List<Card> getTargetableCards(Player ai, SpellAbility sa, Player pl, TargetRestrictions tgt, Player activator, Card source, Game game) {
|
||||||
|
List<Card> hPlay = CardLists.getValidCards(game.getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), activator, source, sa);
|
||||||
|
|
||||||
|
if (activator.equals(ai)) {
|
||||||
|
hPlay = CardLists.filterControlledBy(hPlay, pl);
|
||||||
|
}
|
||||||
|
|
||||||
|
final List<GameObject> objects = Lists.newArrayList(sa.getTargets().getTargets());
|
||||||
|
if (sa.hasParam("TargetUnique")) {
|
||||||
|
objects.addAll(sa.getUniqueTargets());
|
||||||
|
}
|
||||||
|
for (final Object o : objects) {
|
||||||
|
if (o instanceof Card) {
|
||||||
|
final Card c = (Card) o;
|
||||||
|
if (hPlay.contains(c)) {
|
||||||
|
hPlay.remove(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hPlay = CardLists.getTargetableCards(hPlay, sa);
|
||||||
|
return hPlay;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* <p>
|
* <p>
|
||||||
* damageTargetAI.
|
* damageTargetAI.
|
||||||
@@ -344,10 +481,11 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
final PhaseHandler phase = game.getPhaseHandler();
|
final PhaseHandler phase = game.getPhaseHandler();
|
||||||
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
final boolean divided = sa.hasParam("DividedAsYouChoose");
|
||||||
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
Player enemy = ComputerUtil.getOpponentFor(ai);
|
Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||||
|
|
||||||
if ("PowerDmg".equals(sa.getParam("AILogic"))) {
|
if ("PowerDmg".equals(logic)) {
|
||||||
// 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)){
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -366,7 +504,12 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
// target loop
|
// target loop
|
||||||
TargetChoices tcs = sa.getTargets();
|
TargetChoices tcs = sa.getTargets();
|
||||||
|
|
||||||
if ("ChoiceBurn".equals(sa.getParam("AILogic"))) {
|
// Do not use if would kill self
|
||||||
|
if (("SelfDamage".equals(logic)) && (ai.getLife() <= Integer.parseInt(source.getSVar("SelfDamageAmount")))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("ChoiceBurn".equals(logic)) {
|
||||||
// do not waste burns on player if other choices are present
|
// do not waste burns on player if other choices are present
|
||||||
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
|
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
|
||||||
tcs.add(enemy);
|
tcs.add(enemy);
|
||||||
@@ -375,7 +518,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ("Polukranos".equals(sa.getParam("AILogic"))) {
|
if ("Polukranos".equals(logic)) {
|
||||||
int dmgTaken = 0;
|
int dmgTaken = 0;
|
||||||
CardCollection humCreatures = enemy.getCreaturesInPlay();
|
CardCollection humCreatures = enemy.getCreaturesInPlay();
|
||||||
Card lastTgt = null;
|
Card lastTgt = null;
|
||||||
@@ -425,7 +568,27 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
return targetingPlayer.getController().chooseTargetsFor(sa);
|
return targetingPlayer.getController().chooseTargetsFor(sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (tgt.canTgtPlaneswalker()) {
|
||||||
|
// We can damage planeswalkers with this, consider targeting.
|
||||||
|
Card c = this.dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, enemy, false);
|
||||||
|
if (c != null && !this.shouldTgtP(ai, sa, dmg, noPrevention, true)) {
|
||||||
|
tcs.add(c);
|
||||||
|
if (divided) {
|
||||||
|
final int assignedDamage = ComputerUtilCombat.getEnoughDamageToKill(c, dmg, source, false, noPrevention);
|
||||||
|
if (assignedDamage <= dmg) {
|
||||||
|
tgt.addDividedAllocation(c, assignedDamage);
|
||||||
|
}
|
||||||
|
dmg = dmg - assignedDamage;
|
||||||
|
if (dmg <= 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (tgt.canTgtCreatureAndPlayer()) {
|
if (tgt.canTgtCreatureAndPlayer()) {
|
||||||
|
Card c = null;
|
||||||
|
|
||||||
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
|
if (this.shouldTgtP(ai, sa, dmg, noPrevention)) {
|
||||||
tcs.add(enemy);
|
tcs.add(enemy);
|
||||||
@@ -439,7 +602,8 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
dmg = dmg * sa.getTargets().getNumTargeted() / (sa.getTargets().getNumTargeted() +1);
|
dmg = dmg * sa.getTargets().getNumTargeted() / (sa.getTargets().getNumTargeted() +1);
|
||||||
}
|
}
|
||||||
|
|
||||||
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, false);
|
// look for creature targets; currently also catches planeswalkers that can be killed immediately
|
||||||
|
c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, enemy, false);
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
//option to hold removal instead only applies for single targeted removal
|
//option to hold removal instead only applies for single targeted removal
|
||||||
if (sa.isSpell() && !divided && !immediately && tgt.getMaxTargets(sa.getHostCard(), sa) == 1) {
|
if (sa.isSpell() && !divided && !immediately && tgt.getMaxTargets(sa.getHostCard(), sa) == 1) {
|
||||||
@@ -486,7 +650,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (freePing && sa.canTarget(enemy)) {
|
if (freePing && sa.canTarget(enemy) && (!avoidTargetP(ai, sa))) {
|
||||||
tcs.add(enemy);
|
tcs.add(enemy);
|
||||||
if (divided) {
|
if (divided) {
|
||||||
tgt.addDividedAllocation(enemy, dmg);
|
tgt.addDividedAllocation(enemy, dmg);
|
||||||
@@ -518,7 +682,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
} else if ("OppAtTenLife".equals(sa.getParam("AILogic"))) {
|
} else if ("OppAtTenLife".equals(logic)) {
|
||||||
for (final Player p : ai.getOpponents()) {
|
for (final Player p : ai.getOpponents()) {
|
||||||
if (sa.canTarget(p) && p.getLife() == 10 && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
|
if (sa.canTarget(p) && p.getLife() == 10 && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
|
||||||
tcs.add(p);
|
tcs.add(p);
|
||||||
@@ -527,11 +691,13 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
}
|
}
|
||||||
// TODO: Improve Damage, we shouldn't just target the player just
|
// TODO: Improve Damage, we shouldn't just target the player just
|
||||||
// because we can
|
// because we can
|
||||||
else if (sa.canTarget(enemy)) {
|
if (sa.canTarget(enemy) && tcs.getNumTargeted() < tgt.getMaxTargets(source, sa)) {
|
||||||
if ((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|
||||||
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
|
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
|
||||||
|
|| ("PingAfterAttack".equals(logic) && phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) && phase.isPlayerTurn(ai))
|
||||||
|| sa.getPayCosts() == null || immediately
|
|| sa.getPayCosts() == null || immediately
|
||||||
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) {
|
|| this.shouldTgtP(ai, sa, dmg, noPrevention)) &&
|
||||||
|
(!avoidTargetP(ai, sa))) {
|
||||||
tcs.add(enemy);
|
tcs.add(enemy);
|
||||||
if (divided) {
|
if (divided) {
|
||||||
tgt.addDividedAllocation(enemy, dmg);
|
tgt.addDividedAllocation(enemy, dmg);
|
||||||
@@ -579,7 +745,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
if (o instanceof Card) {
|
if (o instanceof Card) {
|
||||||
Card c = (Card) o;
|
Card c = (Card) o;
|
||||||
final int restDamage = ComputerUtilCombat.predictDamageTo(c, dmg, saMe.getHostCard(), false);
|
final int restDamage = ComputerUtilCombat.predictDamageTo(c, dmg, saMe.getHostCard(), false);
|
||||||
if (!c.hasKeyword("Indestructible") && ComputerUtilCombat.getDamageToKill(c) <= restDamage) {
|
if (!c.hasKeyword(Keyword.INDESTRUCTIBLE) && ComputerUtilCombat.getDamageToKill(c) <= restDamage) {
|
||||||
if (c.getController().equals(ai)) {
|
if (c.getController().equals(ai)) {
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
@@ -634,10 +800,21 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
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 = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
System.out.println("damageChooseRequiredTargets " + ai + " " + sa);
|
|
||||||
|
|
||||||
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {
|
||||||
// TODO: Consider targeting the planeswalker
|
if (tgt.canTgtPlaneswalker()) {
|
||||||
|
final Card c = this.dealDamageChooseTgtPW(ai, sa, dmg, noPrevention, ai, mandatory);
|
||||||
|
if (c != null) {
|
||||||
|
sa.getTargets().add(c);
|
||||||
|
if (divided) {
|
||||||
|
tgt.addDividedAllocation(c, dmg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: This currently also catches planeswalkers that can be killed (still necessary? Or can be removed?)
|
||||||
if (tgt.canTgtCreature()) {
|
if (tgt.canTgtCreature()) {
|
||||||
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, ai, mandatory);
|
final Card c = this.dealDamageChooseTgtC(ai, sa, dmg, noPrevention, ai, mandatory);
|
||||||
if (c != null) {
|
if (c != null) {
|
||||||
@@ -768,7 +945,7 @@ public class DamageDealAi extends DamageAiBase {
|
|||||||
for (Card c : creatures) {
|
for (Card c : creatures) {
|
||||||
int power = c.getNetPower();
|
int power = c.getNetPower();
|
||||||
int toughness = c.getNetToughness();
|
int toughness = c.getNetToughness();
|
||||||
boolean canDie = !(c.hasKeyword("Indestructible") || ComputerUtil.canRegenerate(c.getController(), c));
|
boolean canDie = !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || ComputerUtil.canRegenerate(c.getController(), c));
|
||||||
|
|
||||||
// Currently will target creatures with toughness 3+ (or power 5+)
|
// Currently will target creatures with toughness 3+ (or power 5+)
|
||||||
// and only if the creature can actually die, do not "underdrain"
|
// and only if the creature can actually die, do not "underdrain"
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
|||||||
// TODO: improve ai
|
// TODO: improve ai
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
final String svarName = sa.getParam("Execute");
|
SpellAbility trigsa = null;
|
||||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getHostCard().getSVar(svarName), sa.getHostCard());
|
if (sa.hasAdditionalAbility("Execute")) {
|
||||||
|
trigsa = sa.getAdditionalAbility("Execute");
|
||||||
|
} else {
|
||||||
|
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||||
|
}
|
||||||
trigsa.setActivatingPlayer(ai);
|
trigsa.setActivatingPlayer(ai);
|
||||||
|
|
||||||
if (trigsa instanceof AbilitySub) {
|
if (trigsa instanceof AbilitySub) {
|
||||||
@@ -31,8 +35,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
final String svarName = sa.getParam("Execute");
|
SpellAbility trigsa = null;
|
||||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getHostCard().getSVar(svarName), sa.getHostCard());
|
if (sa.hasAdditionalAbility("Execute")) {
|
||||||
|
trigsa = sa.getAdditionalAbility("Execute");
|
||||||
|
} else {
|
||||||
|
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||||
|
}
|
||||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
trigsa.setActivatingPlayer(ai);
|
trigsa.setActivatingPlayer(ai);
|
||||||
|
|
||||||
@@ -45,8 +53,12 @@ public class DelayedTriggerAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final String svarName = sa.getParam("Execute");
|
SpellAbility trigsa = null;
|
||||||
final SpellAbility trigsa = AbilityFactory.getAbility(sa.getSVar(svarName), sa.getHostCard());
|
if (sa.hasAdditionalAbility("Execute")) {
|
||||||
|
trigsa = sa.getAdditionalAbility("Execute");
|
||||||
|
} else {
|
||||||
|
trigsa = AbilityFactory.getAbility(sa.getHostCard(), sa.getParam("Execute"));
|
||||||
|
}
|
||||||
trigsa.setActivatingPlayer(ai);
|
trigsa.setActivatingPlayer(ai);
|
||||||
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,14 @@
|
|||||||
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;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -49,7 +36,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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,10 +51,44 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
hasXCost = abCost.getCostMana() != null ? abCost.getCostMana().getAmountOfX() > 0 : false;
|
hasXCost = abCost.getCostMana() != null ? abCost.getCostMana().getAmountOfX() > 0 : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("AtOpponentsCombatOrAfter".equals(sa.getParam("AILogic"))) {
|
||||||
|
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
|
if (ph.getPlayerTurn() == ai || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("AtEOT".equals(sa.getParam("AILogic"))) {
|
||||||
|
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
|
if (!ph.is(PhaseType.END_OF_TURN)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else if ("AtEOTIfNotAttacking".equals(sa.getParam("AILogic"))) {
|
||||||
|
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||||
|
if (!ph.is(PhaseType.END_OF_TURN) || ai.getAttackedWithCreatureThisTurn()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ability that's intended to destroy own useless token to trigger Grave Pacts
|
||||||
|
// should be fired at end of turn or when under attack after blocking to make opponent sac something
|
||||||
|
boolean havepact = false;
|
||||||
|
|
||||||
|
// TODO replace it with look for a dies -> sacrifice trigger check
|
||||||
|
havepact |= ai.isCardInPlay("Grave Pact");
|
||||||
|
havepact |= ai.isCardInPlay("Butcher of Malakir");
|
||||||
|
havepact |= ai.isCardInPlay("Dictate of Erebos");
|
||||||
|
if ("Pactivator".equals(logic) && havepact) {
|
||||||
|
if ((!ai.getGame().getPhaseHandler().isPlayerTurn(ai))
|
||||||
|
&& ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) || (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)))
|
||||||
|
&& (ai.getOpponents().getCreaturesInPlay().size() > 0)) {
|
||||||
|
ai.getController().chooseTargetsFor(sa);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Targeting
|
// Targeting
|
||||||
if (abTgt != null) {
|
if (abTgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -78,13 +99,18 @@ 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()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (Card c : list) {
|
for (Card c : list) {
|
||||||
if (c.hasKeyword("Indestructible")) {
|
if (c.hasKeyword(Keyword.INDESTRUCTIBLE)) {
|
||||||
sa.getTargets().add(c);
|
sa.getTargets().add(c);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -104,26 +130,14 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
final int cmcMax = ai.hasRevolt() ? 4 : 2;
|
final int cmcMax = ai.hasRevolt() ? 4 : 2;
|
||||||
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
|
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
|
||||||
}
|
}
|
||||||
if (sa.hasParam("AITgts")) {
|
|
||||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
// Filter AI-specific targets if provided
|
||||||
if (source.isEnchanted()) {
|
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||||
if (source.getEnchantedBy(false).get(0).getController().equals(ai)) {
|
|
||||||
return false;
|
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
|
||||||
|
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||||
|
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, false);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
final int value = ComputerUtilCard.evaluateCreature(source);
|
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
|
||||||
@Override
|
|
||||||
public boolean apply(final Card c) {
|
|
||||||
return ComputerUtilCard.evaluateCreature(c) > value + 30;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
list = CardLists.getValidCards(list, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
list = CardLists.getNotKeyword(list, "Indestructible");
|
|
||||||
if (!SpellAbilityAi.playReusable(ai, sa)) {
|
if (!SpellAbilityAi.playReusable(ai, sa)) {
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -147,7 +161,7 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
//Check for undying
|
//Check for undying
|
||||||
return (!c.hasKeyword("Undying") || c.getCounters(CounterType.P1P1) > 0);
|
return (!c.hasKeyword(Keyword.UNDYING) || c.getCounters(CounterType.P1P1) > 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -173,6 +187,11 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
if (hasXCost) {
|
if (hasXCost) {
|
||||||
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
||||||
maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa));
|
maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa));
|
||||||
|
// X can't be more than the lands we have in our hand for "discard X lands"!
|
||||||
|
if ("ScorchedEarth".equals(logic)) {
|
||||||
|
int lands = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||||
|
maxTargets = Math.min(maxTargets, lands);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (sa.hasParam("AIMaxTgtsCount")) {
|
if (sa.hasParam("AIMaxTgtsCount")) {
|
||||||
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified
|
// Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified
|
||||||
@@ -276,7 +295,7 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if (list.isEmpty()
|
if (list.isEmpty()
|
||||||
|| !CardLists.filterControlledBy(list, ai).isEmpty()
|
|| !CardLists.filterControlledBy(list, ai).isEmpty()
|
||||||
|| CardLists.getNotKeyword(list, "Indestructible").isEmpty()) {
|
|| CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -298,8 +317,11 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollection preferred = CardLists.getNotKeyword(list, "Indestructible");
|
CardCollection preferred = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
|
||||||
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
|
preferred = CardLists.filterControlledBy(preferred, ai.getOpponents());
|
||||||
|
if (CardLists.getNotType(preferred, "Creature").isEmpty()) {
|
||||||
|
preferred = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, preferred, false);
|
||||||
|
}
|
||||||
|
|
||||||
// If NoRegen is not set, filter out creatures that have a
|
// If NoRegen is not set, filter out creatures that have a
|
||||||
// regeneration shield
|
// regeneration shield
|
||||||
@@ -314,25 +336,8 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.hasParam("AITgts")) {
|
// Filter AI-specific targets if provided
|
||||||
if (sa.getParam("AITgts").equals("BetterThanSource")) {
|
preferred = ComputerUtil.filterAITgts(sa, ai, (CardCollection)preferred, true);
|
||||||
if (source.isEnchanted()) {
|
|
||||||
if (source.getEnchantedBy(false).get(0).getController().equals(ai)) {
|
|
||||||
preferred.clear();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
final int value = ComputerUtilCard.evaluateCreature(source);
|
|
||||||
preferred = CardLists.filter(preferred, new Predicate<Card>() {
|
|
||||||
@Override
|
|
||||||
public boolean apply(final Card c) {
|
|
||||||
return ComputerUtilCard.evaluateCreature(c) > value + 30;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
preferred = CardLists.getValidCards(preferred, sa.getParam("AITgts"), sa.getActivatingPlayer(), source);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (final Card c : preferred) {
|
for (final Card c : preferred) {
|
||||||
list.remove(c);
|
list.remove(c);
|
||||||
@@ -453,4 +458,5 @@ public class DestroyAi extends SpellAbilityAi {
|
|||||||
return tempoCheck;
|
return tempoCheck;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import forge.game.card.Card;
|
|||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -18,7 +19,7 @@ public class DestroyAllAi extends SpellAbilityAi {
|
|||||||
private static final Predicate<Card> predicate = new Predicate<Card>() {
|
private static final Predicate<Card> predicate = new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return !(c.hasKeyword("Indestructible") || c.getSVar("SacMe").length() > 0);
|
return !(c.hasKeyword(Keyword.INDESTRUCTIBLE) || c.getSVar("SacMe").length() > 0);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,10 +65,15 @@ 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();
|
||||||
|
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||||
Player opponent = ComputerUtil.getOpponentFor(ai); // 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;
|
||||||
|
|
||||||
|
if (logic.equals("Always")) {
|
||||||
|
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
|
||||||
|
}
|
||||||
|
|
||||||
String valid = "";
|
String valid = "";
|
||||||
if (sa.hasParam("ValidCards")) {
|
if (sa.hasParam("ValidCards")) {
|
||||||
valid = sa.getParam("ValidCards");
|
valid = sa.getParam("ValidCards");
|
||||||
@@ -101,6 +107,19 @@ public class DestroyAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
|
||||||
|
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||||
|
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
|
||||||
|
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||||
|
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
|
||||||
|
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// if only creatures are affected evaluate both lists and pass only if
|
// if only creatures are affected evaluate both lists and pass only if
|
||||||
// human creatures are more valuable
|
// human creatures are more valuable
|
||||||
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
|
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.*;
|
||||||
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.card.Card;
|
import forge.game.card.Card;
|
||||||
@@ -46,6 +41,8 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
if ("Never".equals(sa.getParam("AILogic"))) {
|
if ("Never".equals(sa.getParam("AILogic"))) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if ("AtOppEndOfTurn".equals(sa.getParam("AILogic"))) {
|
||||||
|
return game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't deck yourself
|
// don't deck yourself
|
||||||
@@ -127,11 +124,11 @@ public class DigAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> valid, boolean isOptional, Player relatedPlayer) {
|
public Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> valid, boolean isOptional, Player relatedPlayer) {
|
||||||
Card chosen = ComputerUtilCard.getBestAI(valid);
|
|
||||||
if (sa.getActivatingPlayer().isOpponentOf(ai) && relatedPlayer.isOpponentOf(ai)) {
|
if (sa.getActivatingPlayer().isOpponentOf(ai) && relatedPlayer.isOpponentOf(ai)) {
|
||||||
return ComputerUtilCard.getWorstAI(valid);
|
return ComputerUtilCard.getWorstPermanentAI(valid, false, true, false, false);
|
||||||
|
} else {
|
||||||
|
return ComputerUtilCard.getBestAI(valid);
|
||||||
}
|
}
|
||||||
return chosen;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* (non-Javadoc)
|
/* (non-Javadoc)
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import forge.ai.SpellAbilityAi;
|
|||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates;
|
import forge.game.card.CardPredicates;
|
||||||
|
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.spellability.AbilitySub;
|
import forge.game.spellability.AbilitySub;
|
||||||
@@ -14,24 +15,50 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class DigUntilAi extends SpellAbilityAi {
|
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
|
||||||
// never activate EOT)
|
// never activate EOT)
|
||||||
}
|
}
|
||||||
final Random r = MyRandom.getRandom();
|
// if we don't use anything now, we wasted our opportunity.
|
||||||
final boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
if ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN))
|
||||||
|
&& (!ai.getGame().getPhaseHandler().isPlayerTurn(ai))) {
|
||||||
|
chance = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||||
|
|
||||||
Player libraryOwner = ai;
|
Player libraryOwner = ai;
|
||||||
Player opp = ComputerUtil.getOpponentFor(ai);
|
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
|
||||||
|
// until opponent's end of turn phase!
|
||||||
|
// But we still want more (and want to fill grave) if nothing better to do then
|
||||||
|
// This is important for Replenish/Living Death type decks
|
||||||
|
if (!((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN))
|
||||||
|
&& (!ai.getGame().getPhaseHandler().isPlayerTurn(ai)))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (sa.usesTargeting()) {
|
if (sa.usesTargeting()) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
if (!sa.canTarget(opp)) {
|
if (!sa.canTarget(opp)) {
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
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.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;
|
||||||
@@ -17,7 +16,6 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class DiscardAi extends SpellAbilityAi {
|
public class DiscardAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -27,10 +25,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,6 +52,10 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
|
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (aiLogic.equals("VolrathsShapeshifter")) {
|
||||||
|
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
|
||||||
|
}
|
||||||
|
|
||||||
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
|
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
|
||||||
|
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
@@ -97,7 +100,35 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Implement support for Discard AI for cards with AnyNumber set to true.
|
// TODO: Improve support for Discard AI for cards with AnyNumber set to true.
|
||||||
|
if (sa.hasParam("AnyNumber")) {
|
||||||
|
if ("DiscardUncastableAndExcess".equals(aiLogic)) {
|
||||||
|
final CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
|
||||||
|
final int numLandsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size();
|
||||||
|
int numDiscard = 0;
|
||||||
|
int numOppInHand = 0;
|
||||||
|
for (Player p : ai.getGame().getPlayers()) {
|
||||||
|
if (p.getCardsIn(ZoneType.Hand).size() > numOppInHand) {
|
||||||
|
numOppInHand = p.getCardsIn(ZoneType.Hand).size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (Card c : inHand) {
|
||||||
|
if (c.equals(sa.getHostCard())) { continue; }
|
||||||
|
if (c.hasSVar("DoNotDiscardIfAble") || c.hasSVar("IsReanimatorCard")) { continue; }
|
||||||
|
if (c.isCreature() && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getSpellPermanent(), ai)) {
|
||||||
|
numDiscard++;
|
||||||
|
}
|
||||||
|
if ((c.isLand() && numLandsOTB >= 5) || (c.getFirstSpellAbility() != null && !ComputerUtilMana.hasEnoughManaSourcesToCast(c.getFirstSpellAbility(), ai))) {
|
||||||
|
if (numDiscard + 1 <= numOppInHand) {
|
||||||
|
numDiscard++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (numDiscard == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Don't use draw abilities before main 2 if possible
|
// Don't use draw abilities before main 2 if possible
|
||||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
|
||||||
@@ -110,8 +141,7 @@ public class DiscardAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(0.9, sa.getActivationsThisTurn());
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(0.9, sa.getActivationsThisTurn());
|
|
||||||
|
|
||||||
// some other variables here, like handsize vs. maxHandSize
|
// some other variables here, like handsize vs. maxHandSize
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import forge.game.spellability.TargetRestrictions;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class DrainManaAi extends SpellAbilityAi {
|
public class DrainManaAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -21,8 +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 = ComputerUtil.getOpponentFor(ai);
|
final Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
|
||||||
|
|
||||||
if (tgt == null) {
|
if (tgt == null) {
|
||||||
// assume we are looking to tap human's stuff
|
// assume we are looking to tap human's stuff
|
||||||
|
|||||||
@@ -26,10 +26,7 @@ import forge.game.card.Card;
|
|||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates;
|
import forge.game.card.CardPredicates;
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.*;
|
||||||
import forge.game.cost.CostDiscard;
|
|
||||||
import forge.game.cost.CostPart;
|
|
||||||
import forge.game.cost.PaymentDecision;
|
|
||||||
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;
|
||||||
@@ -86,7 +83,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 +123,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("AlwaysAtOppEOT")) {
|
||||||
|
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)) {
|
||||||
@@ -170,7 +177,7 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
int numHand = ai.getCardsIn(ZoneType.Hand).size();
|
int numHand = ai.getCardsIn(ZoneType.Hand).size();
|
||||||
if ("Jace, Vryn's Prodigy".equals(sourceName) && ai.getCardsIn(ZoneType.Graveyard).size() > 3) {
|
if ("Jace, Vryn's Prodigy".equals(sourceName) && ai.getCardsIn(ZoneType.Graveyard).size() > 3) {
|
||||||
return CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANEWALKERS,
|
return CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.PLANESWALKERS,
|
||||||
CardPredicates.isType("Jace")).size() <= 0;
|
CardPredicates.isType("Jace")).size() <= 0;
|
||||||
}
|
}
|
||||||
if (source.isSpell() && ai.getCardsIn(ZoneType.Hand).contains(source)) {
|
if (source.isSpell() && ai.getCardsIn(ZoneType.Hand).contains(source)) {
|
||||||
@@ -204,6 +211,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();
|
||||||
@@ -240,11 +248,38 @@ public class DrawAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Logic for cards that require special handling
|
if (num != null && num.equals("ChosenX")) {
|
||||||
if (sa.hasParam("AILogic")) {
|
if (sa.getSVar("X").equals("XChoice")) {
|
||||||
if ("YawgmothsBargain".equals(sa.getParam("AILogic"))) {
|
// Draw up to max hand size but leave at least 3 in library
|
||||||
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
|
numCards = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
|
||||||
|
|
||||||
|
if (sa.getPayCosts() != null) {
|
||||||
|
if (sa.getPayCosts().hasSpecificCostType(CostPayLife.class)) {
|
||||||
|
// [Necrologia, Pay X Life : Draw X Cards]
|
||||||
|
// Don't draw more than what's "safe" and don't risk a near death experience
|
||||||
|
// Maybe would be better to check for "serious danger" and take more risk?
|
||||||
|
while ((ComputerUtil.aiLifeInDanger(ai, false, numCards) && (numCards > 0))) {
|
||||||
|
numCards--;
|
||||||
}
|
}
|
||||||
|
} else if (sa.getPayCosts().hasSpecificCostType(CostSacrifice.class)) {
|
||||||
|
// [e.g. Krav, the Unredeemed and other cases which say "Sacrifice X creatures: draw X cards]
|
||||||
|
// TODO: Add special logic to limit/otherwise modify the ChosenX value here
|
||||||
|
|
||||||
|
// Skip this ability if nothing is to be chosen for sacrifice
|
||||||
|
if (numCards <= 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sa.setSVar("ChosenX", Integer.toString(numCards));
|
||||||
|
source.setSVar("ChosenX", Integer.toString(numCards));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logic for cards that require special handling
|
||||||
|
if ("YawgmothsBargain".equals(logic)) {
|
||||||
|
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generic logic for all cards that do not need any special handling
|
// Generic logic for all cards that do not need any special handling
|
||||||
@@ -312,6 +347,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);
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
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.collect.Iterables;
|
import com.google.common.collect.Iterables;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
@@ -15,10 +15,9 @@ 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.CardCollection;
|
|
||||||
import forge.game.card.CardLists;
|
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -32,8 +31,7 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(final Player ai,final SpellAbility sa) {
|
protected boolean canPlayAI(final Player ai,final SpellAbility sa) {
|
||||||
final Game game = ai.getGame();
|
final Game game = ai.getGame();
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
|
||||||
boolean randomReturn = r.nextFloat() <= .6667;
|
|
||||||
String logic = "";
|
String logic = "";
|
||||||
|
|
||||||
if (sa.hasParam("AILogic")) {
|
if (sa.hasParam("AILogic")) {
|
||||||
@@ -49,6 +47,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;
|
||||||
@@ -126,7 +139,8 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
|
final int count = CardLists.count(ai.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
return (c.isInstant() || c.isSorcery()) && !c.hasKeyword("Rebound") && ComputerUtil.hasReasonToPlayCardThisTurn(ai, c);
|
return (c.isInstant() || c.isSorcery()) && !c.hasKeyword(Keyword.REBOUND)
|
||||||
|
&& ComputerUtil.hasReasonToPlayCardThisTurn(ai, c);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -137,6 +151,11 @@ public class EffectAi extends SpellAbilityAi {
|
|||||||
randomReturn = true;
|
randomReturn = true;
|
||||||
} else if (logic.equals("Always")) {
|
} else if (logic.equals("Always")) {
|
||||||
randomReturn = true;
|
randomReturn = true;
|
||||||
|
} else if (logic.equals("Main1")) {
|
||||||
|
if (phase.getPhase().isBefore(PhaseType.MAIN1)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
randomReturn = true;
|
||||||
} else if (logic.equals("Main2")) {
|
} else if (logic.equals("Main2")) {
|
||||||
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
|
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -286,4 +305,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);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
63
forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
Normal file
63
forge-ai/src/main/java/forge/ai/ability/ExploreAi.java
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
|
||||||
|
import forge.ai.*;
|
||||||
|
import forge.game.card.*;
|
||||||
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.zone.ZoneType;
|
||||||
|
|
||||||
|
public class ExploreAi extends SpellAbilityAi {
|
||||||
|
/* (non-Javadoc)
|
||||||
|
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
|
// Explore with a target (e.g. Enter the Unknown)
|
||||||
|
if (sa.usesTargeting()) {
|
||||||
|
Card bestCreature = ComputerUtilCard.getBestCreatureAI(aiPlayer.getCardsIn(ZoneType.Battlefield));
|
||||||
|
if (bestCreature == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
sa.resetTargets();
|
||||||
|
sa.getTargets().add(bestCreature);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Card shouldPutInGraveyard(CardCollection top, Player ai) {
|
||||||
|
int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size();
|
||||||
|
CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield);
|
||||||
|
CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand);
|
||||||
|
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||||
|
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
|
||||||
|
|
||||||
|
int maxCMCDiff = 1;
|
||||||
|
int numLandsToStillNeedMore = 2;
|
||||||
|
|
||||||
|
if (ai.getController().isAI()) {
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
maxCMCDiff = aic.getIntProperty(AiProps.EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD);
|
||||||
|
numLandsToStillNeedMore = aic.getIntProperty(AiProps.EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!top.isEmpty()) {
|
||||||
|
Card topCard = top.getFirst();
|
||||||
|
if (landsInHand.isEmpty() && landsOTB.size() <= numLandsToStillNeedMore) {
|
||||||
|
// We need more lands to improve our mana base, explore away the non-lands
|
||||||
|
return topCard;
|
||||||
|
}
|
||||||
|
if (topCard.getCMC() - maxCMCDiff >= predictedMana && !topCard.hasSVar("DoNotDiscardIfAble")) {
|
||||||
|
// We're not casting this in foreseeable future, put it in the graveyard
|
||||||
|
return topCard;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Put on top of the library (do not mark the card for placement in the graveyard)
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -1,20 +1,13 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
import forge.ai.*;
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilAbility;
|
|
||||||
import forge.ai.ComputerUtilCard;
|
|
||||||
import forge.ai.ComputerUtilCombat;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.ability.AbilityFactory;
|
import forge.game.ability.AbilityFactory;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardCollectionView;
|
import forge.game.card.CardCollectionView;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.player.Player;
|
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;
|
||||||
@@ -22,6 +15,9 @@ import forge.game.trigger.Trigger;
|
|||||||
import forge.game.trigger.TriggerType;
|
import forge.game.trigger.TriggerType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class FightAi extends SpellAbilityAi {
|
public class FightAi 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) {
|
||||||
@@ -37,6 +33,12 @@ public class FightAi extends SpellAbilityAi {
|
|||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
|
// everything is defined or targeted above, can't do anything there?
|
||||||
|
if (sa.hasParam("Defined") && !sa.usesTargeting()) {
|
||||||
|
// TODO extend Logic for cards like Arena or Grothama
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Get creature lists
|
// Get creature lists
|
||||||
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
|
CardCollectionView aiCreatures = ai.getCreaturesInPlay();
|
||||||
aiCreatures = CardLists.getTargetableCards(aiCreatures, sa);
|
aiCreatures = CardLists.getTargetableCards(aiCreatures, sa);
|
||||||
@@ -59,6 +61,7 @@ public class FightAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false; // bail at this point, otherwise the AI will overtarget and waste the activation
|
||||||
}
|
}
|
||||||
|
|
||||||
if (sa.hasParam("TargetsFromDifferentZone")) {
|
if (sa.hasParam("TargetsFromDifferentZone")) {
|
||||||
@@ -103,6 +106,11 @@ public class FightAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
|
||||||
|
return checkApiLogic(aiPlayer, sa);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
if (canPlayAI(ai, sa)) {
|
if (canPlayAI(ai, sa)) {
|
||||||
@@ -238,23 +246,24 @@ public class FightAi extends SpellAbilityAi {
|
|||||||
if (!canKill(opponent, fighter, -pumpDefense)) { // can survive
|
if (!canKill(opponent, fighter, -pumpDefense)) { // can survive
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
final Random r = MyRandom.getRandom();
|
if (MyRandom.getRandom().nextInt(20)<(opponent.getCMC() - fighter.getCMC())) { // trade
|
||||||
if (r.nextInt(20)<(opponent.getCMC() - fighter.getCMC())) { // trade
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean canKill(Card fighter, Card opponent, int pumpAttack) {
|
public static boolean canKill(Card fighter, Card opponent, int pumpAttack) {
|
||||||
if (opponent.getSVar("Targeting").equals("Dies")) {
|
if (opponent.getSVar("Targeting").equals("Dies")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed()
|
if (opponent.hasProtectionFrom(fighter) || !opponent.canBeDestroyed() || opponent.getShieldCount() > 0
|
||||||
|| opponent.getShieldCount() > 0 || ComputerUtil.canRegenerate(opponent.getController(), opponent)) {
|
|| ComputerUtil.canRegenerate(opponent.getController(), opponent)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (fighter.hasKeyword("Deathtouch") || ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) {
|
if (fighter.hasKeyword(Keyword.DEATHTOUCH)
|
||||||
|
|| ComputerUtilCombat.getDamageToKill(opponent) <= fighter.getNetPower() + pumpAttack) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtilCombat;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.GameObject;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.card.CardPredicates;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class FogAi extends SpellAbilityAi {
|
public class FogAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -17,6 +22,33 @@ public class FogAi 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();
|
||||||
|
final Card hostCard = sa.getHostCard();
|
||||||
|
|
||||||
|
// Don't cast it, if the effect is already in place
|
||||||
|
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if card would be destroyed, react and use immediately if it's not own turn
|
||||||
|
if ((AiCardMemory.isRememberedCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
|
||||||
|
&& (!game.getStack().isEmpty())
|
||||||
|
&& (!game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()))) {
|
||||||
|
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, null);
|
||||||
|
if (objects.contains(hostCard)) {
|
||||||
|
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_ENEMY_DECLBLK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reserve mana to cast this card if it will be likely needed
|
||||||
|
if (((game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer()))
|
||||||
|
|| (game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)))
|
||||||
|
&& (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT))
|
||||||
|
&& (ComputerUtil.aiLifeInDanger(ai, false, 0))) {
|
||||||
|
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS, true);
|
||||||
|
AiCardMemory.rememberCard(ai, hostCard, AiCardMemory.MemorySet.CHOSEN_FOG_EFFECT);
|
||||||
|
}
|
||||||
|
|
||||||
// AI should only activate this during Human's Declare Blockers phase
|
// AI should only activate this during Human's Declare Blockers phase
|
||||||
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
|
||||||
return false;
|
return false;
|
||||||
@@ -30,9 +62,23 @@ public class FogAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't cast it, if the effect is already in place
|
if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) {
|
||||||
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
|
int dmg = 0;
|
||||||
return false;
|
for (Card atk : game.getCombat().getAttackersOf(ai)) {
|
||||||
|
if (game.getCombat().isUnblocked(atk)) {
|
||||||
|
dmg += atk.getNetCombatDamage();
|
||||||
|
} else if (atk.hasKeyword(Keyword.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
|
||||||
|
|||||||
@@ -7,8 +7,6 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class LifeExchangeAi extends SpellAbilityAi {
|
public class LifeExchangeAi extends SpellAbilityAi {
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -21,7 +19,6 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
final int myLife = aiPlayer.getLife();
|
final int myLife = aiPlayer.getLife();
|
||||||
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
|
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
|
||||||
final int hLife = opponent.getLife();
|
final int hLife = opponent.getLife();
|
||||||
@@ -31,7 +28,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
boolean chance = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* TODO - There is one card that takes two targets (Soul Conduit)
|
* TODO - There is one card that takes two targets (Soul Conduit)
|
||||||
@@ -58,7 +55,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
|||||||
// cost includes sacrifice probably, so make sure it's worth it
|
// cost includes sacrifice probably, so make sure it's worth it
|
||||||
chance &= (hLife > (myLife + 8));
|
chance &= (hLife > (myLife + 8));
|
||||||
|
|
||||||
return ((r.nextFloat() < .6667) && chance);
|
return ((MyRandom.getRandom().nextFloat() < .6667) && chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,141 @@
|
|||||||
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import forge.ai.ComputerUtil;
|
||||||
|
import forge.ai.ComputerUtilAbility;
|
||||||
|
import forge.ai.ComputerUtilCombat;
|
||||||
|
import forge.ai.SpellAbilityAi;
|
||||||
|
import forge.game.Game;
|
||||||
|
import forge.game.card.Card;
|
||||||
|
import forge.game.player.Player;
|
||||||
|
import forge.game.spellability.SpellAbility;
|
||||||
|
import forge.game.spellability.TargetRestrictions;
|
||||||
|
import forge.game.zone.MagicStack;
|
||||||
|
|
||||||
|
public class LifeExchangeVariantAi extends SpellAbilityAi {
|
||||||
|
|
||||||
|
/*
|
||||||
|
* (non-Javadoc)
|
||||||
|
*
|
||||||
|
* @see
|
||||||
|
* forge.card.abilityfactory.AbilityFactoryAlterLife.SpellAiLogic#canPlayAI
|
||||||
|
* (forge.game.player.Player, java.util.Map,
|
||||||
|
* forge.card.spellability.SpellAbility)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||||
|
final Game game = ai.getGame();
|
||||||
|
|
||||||
|
if ("Tree of Redemption".equals(sourceName)) {
|
||||||
|
if (!ai.canGainLife())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// someone controls "Rain of Gore" or "Sulfuric Vortex", lifegain is bad in that case
|
||||||
|
if (game.isCardInPlay("Rain of Gore") || game.isCardInPlay("Sulfuric Vortex"))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// an opponent controls "Tainted Remedy", lifegain is bad in that case
|
||||||
|
for (Player op : ai.getOpponents()) {
|
||||||
|
if (op.isCardInPlay("Tainted Remedy"))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ComputerUtil.waitForBlocking(sa) || ai.getLife() + 1 >= source.getNetToughness()
|
||||||
|
|| (ai.getLife() > 5 && !ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if ("Tree of Perdition".equals(sourceName)) {
|
||||||
|
boolean shouldDo = false;
|
||||||
|
|
||||||
|
if (ComputerUtil.waitForBlocking(sa))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
for (Player op : ai.getOpponents()) {
|
||||||
|
// if oppoent can't be targeted, or it can't lose life, try another one
|
||||||
|
if (!op.canBeTargetedBy(sa) || !op.canLoseLife())
|
||||||
|
continue;
|
||||||
|
// an opponent has more live than this toughness
|
||||||
|
if (op.getLife() + 1 >= source.getNetToughness()) {
|
||||||
|
shouldDo = true;
|
||||||
|
} else {
|
||||||
|
// opponent can't gain life, so "Tainted Remedy" should not work.
|
||||||
|
if (!op.canGainLife()) {
|
||||||
|
continue;
|
||||||
|
} else if (ai.isCardInPlay("Tainted Remedy")) { // or AI has Tainted Remedy
|
||||||
|
shouldDo = true;
|
||||||
|
} else {
|
||||||
|
for (Player ally : ai.getAllies()) {
|
||||||
|
// if an Ally has Tainted Remedy and opponent is also opponent of ally
|
||||||
|
if (ally.isCardInPlay("Tainted Remedy") && op.isOpponentOf(ally))
|
||||||
|
shouldDo = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldDo) {
|
||||||
|
sa.getTargets().add(op);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return shouldDo;
|
||||||
|
}
|
||||||
|
else if ("Evra, Halcyon Witness".equals(sourceName)) {
|
||||||
|
if (!ai.canGainLife())
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int aiLife = ai.getLife();
|
||||||
|
|
||||||
|
if (source.getNetPower() > aiLife) {
|
||||||
|
if (ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat())) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check the top of stack
|
||||||
|
MagicStack stack = ai.getGame().getStack();
|
||||||
|
if (!stack.isEmpty()) {
|
||||||
|
SpellAbility saTop = stack.peekAbility();
|
||||||
|
if (ComputerUtil.predictDamageFromSpell(saTop, ai) >= aiLife) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <p>
|
||||||
|
* exchangeLifeDoTriggerAINoCost.
|
||||||
|
* </p>
|
||||||
|
* @param sa
|
||||||
|
* a {@link forge.game.spellability.SpellAbility} object.
|
||||||
|
* @param mandatory
|
||||||
|
* a boolean.
|
||||||
|
* @param af
|
||||||
|
* a {@link forge.game.ability.AbilityFactory} object.
|
||||||
|
*
|
||||||
|
* @return a boolean.
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa,
|
||||||
|
final boolean mandatory) {
|
||||||
|
|
||||||
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
Player opp = ComputerUtil.getOpponentFor(ai);
|
||||||
|
if (tgt != null) {
|
||||||
|
sa.resetTargets();
|
||||||
|
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||||
|
sa.getTargets().add(opp);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)) {
|
||||||
|
|||||||
@@ -13,13 +13,10 @@ import forge.game.spellability.SpellAbility;
|
|||||||
import forge.game.spellability.TargetRestrictions;
|
import forge.game.spellability.TargetRestrictions;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class LifeSetAi extends SpellAbilityAi {
|
public class LifeSetAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
// 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();
|
||||||
@@ -56,7 +53,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
final boolean chance = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
final boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
@@ -94,7 +91,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (amount < myLife) {
|
if (amount <= myLife) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -104,7 +101,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((r.nextFloat() < .6667) && chance);
|
return ((MyRandom.getRandom().nextFloat() < .6667) && chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -127,6 +124,12 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
amount = AbilityUtils.calculateAmount(sa.getHostCard(), amountStr, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// special cases when amount can't be calculated without targeting first
|
||||||
|
if (amount == 0 && "TargetedPlayer$StartingLife/HalfDown".equals(source.getSVar(amountStr))) {
|
||||||
|
// e.g. Torgaar, Famine Incarnate
|
||||||
|
return doHalfStartingLifeLogic(ai, opponent, sa);
|
||||||
|
}
|
||||||
|
|
||||||
if (sourceName.equals("Eternity Vessel")
|
if (sourceName.equals("Eternity Vessel")
|
||||||
&& (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterType.CHARGE) == 0))) {
|
&& (opponent.isCardInPlay("Vampire Hexmage") || (source.getCounters(CounterType.CHARGE) == 0))) {
|
||||||
return false;
|
return false;
|
||||||
@@ -134,8 +137,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// If the Target is gaining life, target self.
|
// If the Target is gaining life, target self.
|
||||||
// if the Target is modifying how much life is gained, this needs to
|
// if the Target is modifying how much life is gained, this needs to
|
||||||
// be
|
// be handled better
|
||||||
// handled better
|
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
if (tgt != null) {
|
if (tgt != null) {
|
||||||
sa.resetTargets();
|
sa.resetTargets();
|
||||||
@@ -157,4 +159,36 @@ public class LifeSetAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean doHalfStartingLifeLogic(Player ai, Player opponent, SpellAbility sa) {
|
||||||
|
int aiAmount = ai.getStartingLife() / 2;
|
||||||
|
int oppAmount = opponent.getStartingLife() / 2;
|
||||||
|
int aiLife = ai.getLife();
|
||||||
|
int oppLife = opponent.getLife();
|
||||||
|
|
||||||
|
sa.resetTargets();
|
||||||
|
|
||||||
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
if (tgt != null) {
|
||||||
|
if (tgt.canOnlyTgtOpponent()) {
|
||||||
|
if (oppLife > oppAmount) {
|
||||||
|
sa.getTargets().add(opponent);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (aiAmount > ai.getLife() && aiLife < 5) {
|
||||||
|
sa.getTargets().add(ai);
|
||||||
|
} else if (oppLife > oppAmount) {
|
||||||
|
sa.getTargets().add(opponent);
|
||||||
|
} else if (aiAmount > aiLife) {
|
||||||
|
sa.getTargets().add(ai);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,26 +1,21 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
import forge.ai.AiPlayDecision;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilAbility;
|
|
||||||
import forge.ai.ComputerUtilCost;
|
|
||||||
import forge.ai.ComputerUtilMana;
|
|
||||||
import forge.ai.PlayerControllerAi;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.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.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.keyword.Keyword;
|
||||||
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.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -110,13 +105,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"))) {
|
||||||
@@ -156,9 +166,12 @@ public class ManaEffectAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
SpellAbility testSaNoCost = testSa.copyWithNoManaCost();
|
SpellAbility testSaNoCost = testSa.copyWithNoManaCost();
|
||||||
|
if (testSaNoCost == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
testSaNoCost.setActivatingPlayer(ai);
|
testSaNoCost.setActivatingPlayer(ai);
|
||||||
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSaNoCost) == AiPlayDecision.WillPlay) {
|
if (((PlayerControllerAi)ai.getController()).getAi().canPlaySa(testSaNoCost) == AiPlayDecision.WillPlay) {
|
||||||
if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword("Haste")
|
if (testSa.getHostCard().isPermanent() && !testSa.getHostCard().hasKeyword(Keyword.HASTE)
|
||||||
&& !ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
|
&& !ai.getGame().getPhaseHandler().is(PhaseType.MAIN2)) {
|
||||||
// AI will waste a ritual in Main 1 unless the casted permanent is a haste creature
|
// AI will waste a ritual in Main 1 unless the casted permanent is a haste creature
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import forge.ai.ComputerUtil;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.ComputerUtilCost;
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
@@ -14,13 +17,13 @@ import forge.game.card.CardCollection;
|
|||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates;
|
import forge.game.card.CardPredicates;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.keyword.KeywordInterface;
|
||||||
import forge.game.mana.ManaCostBeingPaid;
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
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.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
|
||||||
|
|
||||||
public class PermanentAi extends SpellAbilityAi {
|
public class PermanentAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -32,7 +35,7 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
final Card card = sa.getHostCard();
|
final Card card = sa.getHostCard();
|
||||||
|
|
||||||
if (card.hasStartOfKeyword("You may cast CARDNAME as though it had flash. If") && !ai.couldCastSorcery(sa)) {
|
if (card.hasKeyword("MayFlashSac") && !ai.couldCastSorcery(sa)) {
|
||||||
// AiPlayDecision.AnotherTime
|
// AiPlayDecision.AnotherTime
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -62,9 +65,11 @@ 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.PLANESWALKERS);
|
||||||
for (String type : card.getType().getSubtypes()) { // determine
|
for (String type : card.getType().getSubtypes()) { // determine
|
||||||
// planewalker
|
// planewalker
|
||||||
// subtype
|
// subtype
|
||||||
@@ -75,7 +80,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");
|
||||||
@@ -142,7 +147,8 @@ public class PermanentAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// don't play cards without being able to pay the upkeep for
|
// don't play cards without being able to pay the upkeep for
|
||||||
for (String ability : card.getKeywords()) {
|
for (KeywordInterface inst : card.getKeywords()) {
|
||||||
|
String ability = inst.getOriginal();
|
||||||
if (ability.startsWith("UpkeepCost")) {
|
if (ability.startsWith("UpkeepCost")) {
|
||||||
final String[] k = ability.split(":");
|
final String[] k = ability.split(":");
|
||||||
final String costs = k[1];
|
final String costs = k[1];
|
||||||
@@ -175,22 +181,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.
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ public class PermanentCreatureAi extends PermanentAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// save cards with flash for surprise blocking
|
// save cards with flash for surprise blocking
|
||||||
if (card.hasKeyword("Flash")
|
if (card.withFlash(ai)
|
||||||
&& (ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
|
&& (ai.isUnlimitedHandSize() || ai.getCardsIn(ZoneType.Hand).size() <= ai.getMaxHandSize()
|
||||||
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
|| ph.getPhase().isBefore(PhaseType.END_OF_TURN))
|
||||||
&& ai.getManaPool().totalMana() <= 0
|
&& ai.getManaPool().totalMana() <= 0
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import com.google.common.base.Predicates;
|
import com.google.common.base.Predicates;
|
||||||
|
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;
|
||||||
@@ -16,7 +17,6 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class PhasesAi extends SpellAbilityAi {
|
public class PhasesAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
@@ -25,14 +25,17 @@ public class PhasesAi extends SpellAbilityAi {
|
|||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
|
||||||
|
|
||||||
List<Card> tgtCards;
|
List<Card> tgtCards;
|
||||||
if (tgt == null) {
|
if (tgt == null) {
|
||||||
tgtCards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
tgtCards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||||
if (tgtCards.contains(source)) {
|
if (tgtCards.contains(source)) {
|
||||||
// Protect it from something
|
// Protect it from something
|
||||||
|
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(aiPlayer, null, true).contains(source);
|
||||||
|
if (isThreatened) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Card def = tgtCards.get(0);
|
// Card def = tgtCards.get(0);
|
||||||
// Phase this out if it might attack me, or before it can be
|
// Phase this out if it might attack me, or before it can be
|
||||||
|
|||||||
@@ -1,26 +1,26 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
import forge.ai.*;
|
||||||
import forge.ai.AiPlayDecision;
|
import forge.card.CardStateName;
|
||||||
import forge.ai.ComputerUtil;
|
import forge.card.CardTypeView;
|
||||||
import forge.ai.ComputerUtilCard;
|
|
||||||
import forge.ai.PlayerControllerAi;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.GameType;
|
||||||
import forge.game.ability.AbilityUtils;
|
import forge.game.ability.AbilityUtils;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.player.Player;
|
import forge.game.player.Player;
|
||||||
import forge.game.player.PlayerActionConfirmMode;
|
import forge.game.player.PlayerActionConfirmMode;
|
||||||
import forge.game.spellability.Spell;
|
import forge.game.spellability.Spell;
|
||||||
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 forge.util.MyRandom;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class PlayAi extends SpellAbilityAi {
|
public class PlayAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -55,10 +55,36 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (game.getRules().hasAppliedVariant(GameType.MoJhoSto) && source.getName().equals("Jhoira of the Ghitu Avatar")) {
|
||||||
|
// Additional logic for MoJhoSto:
|
||||||
|
// Do not activate Jhoira too early, usually there are few good targets
|
||||||
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA);
|
||||||
|
int chanceToActivateInst = 100 - aic.getIntProperty(AiProps.MOJHOSTO_CHANCE_TO_USE_JHOIRA_COPY_INSTANT);
|
||||||
|
if (ai.getLandsInPlay().size() < numLandsForJhoira) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Don't spam activate the Instant copying ability all the time to give the AI a chance to use other abilities
|
||||||
|
// Can probably be improved, but as random as MoJhoSto already is, probably not a huge deal for now
|
||||||
|
if ("Instant".equals(sa.getParam("AnySupportedCard")) && MyRandom.percentTrue(chanceToActivateInst)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ("ReplaySpell".equals(logic)) {
|
if ("ReplaySpell".equals(logic)) {
|
||||||
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
|
return ComputerUtil.targetPlayableSpellCard(ai, cards, sa, sa.hasParam("WithoutManaCost"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (source != null && source.hasKeyword(Keyword.HIDEAWAY) && source.hasRemembered()) {
|
||||||
|
// AI is not very good at playing non-permanent spells this way, at least yet
|
||||||
|
// (might be possible to enable it for Sorceries in Main1/Main2 if target is available,
|
||||||
|
// but definitely not for most Instants)
|
||||||
|
Card rem = (Card) source.getFirstRemembered();
|
||||||
|
CardTypeView t = rem.getState(CardStateName.Original).getType();
|
||||||
|
|
||||||
|
return t.isPermanent() && !t.isLand();
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +131,7 @@ public class PlayAi extends SpellAbilityAi {
|
|||||||
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
for (SpellAbility s : c.getBasicSpells()) {
|
for (SpellAbility s : c.getBasicSpells(c.getState(CardStateName.Original))) {
|
||||||
Spell spell = (Spell) s;
|
Spell spell = (Spell) s;
|
||||||
s.setActivatingPlayer(ai);
|
s.setActivatingPlayer(ai);
|
||||||
// timing restrictions still apply
|
// timing restrictions still apply
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package forge.ai.ability;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
@@ -151,8 +150,7 @@ public class ProtectAi extends SpellAbilityAi {
|
|||||||
Combat combat = ai.getGame().getCombat();
|
Combat combat = ai.getGame().getCombat();
|
||||||
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
|
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
|
||||||
float ratio = 1.0f * dmg / opponent.getLife();
|
float ratio = 1.0f * dmg / opponent.getLife();
|
||||||
Random r = MyRandom.getRandom();
|
return MyRandom.getRandom().nextFloat() < ratio;
|
||||||
return r.nextFloat() < ratio;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -177,6 +175,11 @@ public class ProtectAi extends SpellAbilityAi {
|
|||||||
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||||
if (cards.size() == 0) {
|
if (cards.size() == 0) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if (cards.size() == 1) {
|
||||||
|
// Affecting single card
|
||||||
|
if ((getProtectCreatures(ai, sa)).contains(cards.get(0))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
/*
|
/*
|
||||||
* when this happens we need to expand AI to consider if its ok
|
* when this happens we need to expand AI to consider if its ok
|
||||||
|
|||||||
@@ -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,33 +1,20 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicates;
|
|
||||||
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.keyword.Keyword;
|
||||||
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 +22,13 @@ import forge.game.player.PlayerActionConfirmMode;
|
|||||||
import forge.game.spellability.SpellAbility;
|
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.staticability.StaticAbility;
|
||||||
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 {
|
||||||
|
|
||||||
@@ -43,12 +36,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
if (cost == null) {
|
if (cost == null) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
for (final CostPart part : cost.getCostParts()) {
|
return cost.hasSpecificCostType(CostTapType.class);
|
||||||
if (part instanceof CostTapType) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -83,16 +71,19 @@ 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);
|
||||||
|
} else if (aiLogic.startsWith("AristocratCounters")) {
|
||||||
|
return doAristocratWithCountersLogic(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;
|
||||||
@@ -102,6 +93,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);
|
||||||
}
|
}
|
||||||
@@ -113,7 +109,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -137,12 +133,16 @@ public class PumpAi extends PumpAiBase {
|
|||||||
final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : "";
|
final String numDefense = sa.hasParam("NumDef") ? sa.getParam("NumDef") : "";
|
||||||
final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : "";
|
final String numAttack = sa.hasParam("NumAtt") ? sa.getParam("NumAtt") : "";
|
||||||
|
|
||||||
final String aiLogic = sa.getParam("AILogic");
|
final String aiLogic = sa.getParamOrDefault("AILogic", "");
|
||||||
|
|
||||||
final boolean isFight = "Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic);
|
final boolean 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 (aiLogic.startsWith("AristocratCounters")) {
|
||||||
|
return true; // the preconditions to this are already tested in checkAiLogic
|
||||||
|
} else if ("MoveCounter".equals(aiLogic)) {
|
||||||
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
|
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
|
||||||
|
|
||||||
if (moveSA == null) {
|
if (moveSA == null) {
|
||||||
@@ -186,7 +186,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
|
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
|
||||||
|
|
||||||
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
|
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
|
||||||
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying")
|
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING)
|
||||||
|| card.isToken()) {
|
|| card.isToken()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -239,7 +239,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
|
srcCardCpy.setCounters(cType, srcCardCpy.getCounters(cType) - amount);
|
||||||
|
|
||||||
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
|
if (CounterType.P1P1.equals(cType) && srcCardCpy.getNetToughness() <= 0) {
|
||||||
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword("Undying")
|
if (srcCardCpy.getCounters(cType) > 0 || !card.hasKeyword(Keyword.UNDYING)
|
||||||
|| card.isToken()) {
|
|| card.isToken()) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -334,6 +334,23 @@ public class PumpAi extends PumpAiBase {
|
|||||||
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
|
attack = AbilityUtils.calculateAmount(sa.getHostCard(), numAttack, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("ContinuousBonus".equals(aiLogic)) {
|
||||||
|
// P/T bonus in a continuous static ability
|
||||||
|
for (StaticAbility stAb : source.getStaticAbilities()) {
|
||||||
|
if ("Continuous".equals(stAb.getParam("Mode"))) {
|
||||||
|
if (stAb.hasParam("AddPower")) {
|
||||||
|
attack += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
|
||||||
|
}
|
||||||
|
if (stAb.hasParam("AddToughness")) {
|
||||||
|
defense += AbilityUtils.calculateAmount(source, stAb.getParam("AddToughness"), stAb);
|
||||||
|
}
|
||||||
|
if (stAb.hasParam("AddKeyword")) {
|
||||||
|
keywords.addAll(Lists.newArrayList(stAb.getParam("AddKeyword").split(" & ")));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0 && !isBerserk)) {
|
if ((numDefense.contains("X") && defense == 0) || (numAttack.contains("X") && attack == 0 && !isBerserk)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -360,9 +377,21 @@ 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)) {
|
||||||
|
|
||||||
|
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
|
||||||
|
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)
|
||||||
|
|| game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
|
||||||
|
if (!ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, pumped)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -372,6 +401,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()
|
||||||
|
|
||||||
@@ -406,10 +450,28 @@ public class PumpAi extends PumpAiBase {
|
|||||||
|
|
||||||
CardCollection list;
|
CardCollection list;
|
||||||
if (sa.hasParam("AILogic")) {
|
if (sa.hasParam("AILogic")) {
|
||||||
if (sa.getParam("AILogic").equals("HighestPower")) {
|
if (sa.getParam("AILogic").equals("HighestPower") || sa.getParam("AILogic").equals("ContinuousBonus")) {
|
||||||
list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
|
list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
|
||||||
list = CardLists.getTargetableCards(list, sa);
|
list = CardLists.getTargetableCards(list, sa);
|
||||||
CardLists.sortByPowerDesc(list);
|
CardLists.sortByPowerDesc(list);
|
||||||
|
|
||||||
|
// Try not to kill own creatures with this pump
|
||||||
|
CardCollection canDieToPump = new CardCollection();
|
||||||
|
for (Card c : list) {
|
||||||
|
if (c.isCreature() && c.getController() == ai
|
||||||
|
&& c.getNetToughness() - c.getTempToughnessBoost() + defense <= 0) {
|
||||||
|
canDieToPump.add(c);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.removeAll(canDieToPump);
|
||||||
|
|
||||||
|
// Generally, don't pump anything that your opponents control
|
||||||
|
if ("ContinuousBonus".equals(sa.getParam("AILogic"))) {
|
||||||
|
// TODO: make it possible for the AI to use this logic to kill opposing creatures
|
||||||
|
// when a toughness debuff is applied
|
||||||
|
list = CardLists.filter(list, CardPredicates.isController(ai));
|
||||||
|
}
|
||||||
|
|
||||||
if (!list.isEmpty()) {
|
if (!list.isEmpty()) {
|
||||||
sa.getTargets().add(list.get(0));
|
sa.getTargets().add(list.get(0));
|
||||||
return true;
|
return true;
|
||||||
@@ -478,6 +540,9 @@ public class PumpAi extends PumpAiBase {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filter AI-specific targets if provided
|
||||||
|
list = ComputerUtil.filterAITgts(sa, ai, (CardCollection)list, true);
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
if (ComputerUtil.activateForCost(sa, ai)) {
|
if (ComputerUtil.activateForCost(sa, ai)) {
|
||||||
return pumpMandatoryTarget(ai, sa);
|
return pumpMandatoryTarget(ai, sa);
|
||||||
@@ -490,6 +555,17 @@ public class PumpAi extends PumpAiBase {
|
|||||||
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
list = ComputerUtil.getSafeTargets(ai, sa, list);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ("BetterCreatureThanSource".equals(sa.getParam("AILogic"))) {
|
||||||
|
// Don't target cards that are not better in value than the targeting card
|
||||||
|
final int sourceValue = ComputerUtilCard.evaluateCreature(source);
|
||||||
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return card.isCreature() && ComputerUtilCard.evaluateCreature(card) > sourceValue + 30;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ("Snapcaster".equals(sa.getParam("AILogic"))) {
|
if ("Snapcaster".equals(sa.getParam("AILogic"))) {
|
||||||
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
|
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -695,7 +771,7 @@ public class PumpAi extends PumpAiBase {
|
|||||||
|
|
||||||
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
|
if ((sa.getTargetRestrictions() == null) || !sa.getTargetRestrictions().doesTarget()) {
|
||||||
if (source.isCreature()) {
|
if (source.isCreature()) {
|
||||||
if (!source.hasKeyword("Indestructible") && source.getNetToughness() + defense <= source.getDamage()) {
|
if (!source.hasKeyword(Keyword.INDESTRUCTIBLE) && source.getNetToughness() + defense <= source.getDamage()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (source.getNetToughness() + defense <= 0) {
|
if (source.getNetToughness() + defense <= 0) {
|
||||||
@@ -712,8 +788,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) {
|
||||||
//TODO Add logic here if necessary but I think the AI won't cast
|
//TODO Add logic here if necessary but I think the AI won't cast
|
||||||
@@ -721,4 +795,248 @@ 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 boolean indestructible = sa.hasParam("KW") && sa.getParam("KW").contains("Indestructible");
|
||||||
|
final int selfEval = ComputerUtilCard.evaluateCreature(source);
|
||||||
|
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
|
||||||
|
|
||||||
|
if (numOtherCreats == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to save the card from death by pumping it if it's threatened with a damage spell
|
||||||
|
if (isThreatened && (toughnessBonus > 0 || indestructible)) {
|
||||||
|
SpellAbility saTop = game.getStack().peekAbility();
|
||||||
|
|
||||||
|
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
|
||||||
|
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage();
|
||||||
|
final int numCreatsToSac = indestructible ? 1 : Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus));
|
||||||
|
|
||||||
|
if (numCreatsToSac > 1) { // probably not worth sacrificing too much
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (indestructible || (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg)) {
|
||||||
|
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
|
||||||
|
new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||||
|
|| card.hasSVar("SacMe")
|
||||||
|
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (sacFodder.size() >= numCreatsToSac) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combat == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combat.isAttacking(source)) {
|
||||||
|
if (combat.getBlockers(source).isEmpty()) {
|
||||||
|
// Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if
|
||||||
|
// the opponent is tapped out or if we're willing to risk it (will currently risk it
|
||||||
|
// in case it sacs less than half its creatures to deal lethal damage)
|
||||||
|
|
||||||
|
// TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully
|
||||||
|
// for potential damage prevention, various effects like reducing damage to 0, etc.)
|
||||||
|
|
||||||
|
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
|
||||||
|
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
|
||||||
|
|
||||||
|
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
|
||||||
|
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||||
|
|
||||||
|
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
|
||||||
|
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||||
|
}
|
||||||
|
|
||||||
|
final int numCreatsToSac = indestructible ? 1 : (lethalDmg - source.getNetCombatDamage()) / powerBonus;
|
||||||
|
|
||||||
|
if (defTappedOut || numCreatsToSac < numOtherCreats / 2) {
|
||||||
|
return source.getNetCombatDamage() < lethalDmg
|
||||||
|
&& source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||||
|
// than the card we attacked with.
|
||||||
|
final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(),
|
||||||
|
new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||||
|
|| ComputerUtilCard.evaluateCreature(card) < selfEval;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sacTgts.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
|
||||||
|
final int DefP = indestructible ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
|
||||||
|
|
||||||
|
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
|
||||||
|
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||||
|
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
|
||||||
|
new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||||
|
|| card.hasSVar("SacMe")
|
||||||
|
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return !sacFodder.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean doAristocratWithCountersLogic(final SpellAbility sa, final Player ai) {
|
||||||
|
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
|
||||||
|
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
|
||||||
|
|
||||||
|
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
|
||||||
|
if (numOtherCreats == 0) {
|
||||||
|
// Cut short if there's nothing to sac at all
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the standard Aristocrats logic applies first (if in the right conditions for it)
|
||||||
|
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
|
||||||
|
if (isDeclareBlockers || isThreatened) {
|
||||||
|
if (doAristocratLogic(sa, ai)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if anything is to be gained from the PutCounter subability
|
||||||
|
if (sa.getSubAbility() == null || sa.getSubAbility().getApi() != ApiType.PutCounter) {
|
||||||
|
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
|
||||||
|
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter subability!");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final Game game = ai.getGame();
|
||||||
|
final Combat combat = game.getCombat();
|
||||||
|
final int selfEval = ComputerUtilCard.evaluateCreature(source);
|
||||||
|
|
||||||
|
String typeToGainCtr = "";
|
||||||
|
if (logic.contains(".")) {
|
||||||
|
typeToGainCtr = logic.substring(logic.indexOf(".") + 1);
|
||||||
|
}
|
||||||
|
CardCollection relevantCreats = typeToGainCtr.isEmpty() ? ai.getCreaturesInPlay()
|
||||||
|
: CardLists.filter(ai.getCreaturesInPlay(), CardPredicates.isType(typeToGainCtr));
|
||||||
|
relevantCreats.remove(source);
|
||||||
|
if (relevantCreats.isEmpty()) {
|
||||||
|
// No relevant creatures to sac
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int numCtrs = AbilityUtils.calculateAmount(source, sa.getSubAbility().getParam("CounterNum"), sa.getSubAbility());
|
||||||
|
|
||||||
|
if (combat != null && combat.isAttacking(source) && isDeclareBlockers) {
|
||||||
|
if (combat.getBlockers(source).isEmpty()) {
|
||||||
|
// Unblocked. Check if we can deal lethal after receiving counters.
|
||||||
|
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
|
||||||
|
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
|
||||||
|
|
||||||
|
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
|
||||||
|
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
|
||||||
|
|
||||||
|
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
|
||||||
|
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if there's anything that will die anyway that can be eaten to gain a perma-bonus
|
||||||
|
final CardCollection forcedSacTgts = CardLists.filter(relevantCreats,
|
||||||
|
new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
|
||||||
|
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
if (!forcedSacTgts.isEmpty()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
|
||||||
|
|
||||||
|
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
|
||||||
|
return source.getNetCombatDamage() < lethalDmg
|
||||||
|
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
|
||||||
|
// than the card we attacked with. Since we're getting a permanent bonus, consider sacrificing
|
||||||
|
// things that are also threatened to be destroyed anyway.
|
||||||
|
final CardCollection sacTgts = CardLists.filter(relevantCreats,
|
||||||
|
new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||||
|
|| ComputerUtilCard.evaluateCreature(card) < selfEval
|
||||||
|
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (sacTgts.isEmpty()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
final boolean sourceCantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
|
||||||
|
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
|
||||||
|
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
|
||||||
|
|
||||||
|
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
|
||||||
|
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
|
||||||
|
final boolean isBlocking = combat != null && combat.isBlocking(source);
|
||||||
|
final CardCollection sacFodder = CardLists.filter(relevantCreats,
|
||||||
|
new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return ComputerUtilCard.isUselessCreature(ai, card)
|
||||||
|
|| card.hasSVar("SacMe")
|
||||||
|
|| (isBlocking && ComputerUtilCard.evaluateCreature(card) < selfEval)
|
||||||
|
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return !sacFodder.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,16 @@ package forge.ai.ability;
|
|||||||
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 forge.ai.ComputerUtil;
|
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;
|
||||||
import forge.card.MagicColor;
|
import forge.card.MagicColor;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
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.CardUtil;
|
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.combat.CombatUtil;
|
import forge.game.combat.CombatUtil;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
import forge.game.phase.PhaseHandler;
|
import forge.game.phase.PhaseHandler;
|
||||||
import forge.game.phase.PhaseType;
|
import forge.game.phase.PhaseType;
|
||||||
import forge.game.phase.Untap;
|
import forge.game.phase.Untap;
|
||||||
@@ -140,7 +136,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
|| card.getNetCombatDamage() <= 0
|
|| card.getNetCombatDamage() <= 0
|
||||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||||
|| ph.getPhase().isBefore(PhaseType.MAIN1)
|
|| ph.getPhase().isBefore(PhaseType.MAIN1)
|
||||||
|| CardLists.getNotKeyword(ai.getCreaturesInPlay(), "Defender").isEmpty())) {
|
|| CardLists.getNotKeyword(ai.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty())) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!ph.isPlayerTurn(ai) && (combat == null || !combat.isAttacking(card) || card.getNetCombatDamage() <= 0)) {
|
if (!ph.isPlayerTurn(ai) && (combat == null || !combat.isAttacking(card) || card.getNetCombatDamage() <= 0)) {
|
||||||
@@ -198,15 +194,33 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.endsWith("Flying")) {
|
} else if (keyword.endsWith("Flying")) {
|
||||||
|
CardCollectionView attackingFlyer = CardCollection.EMPTY;
|
||||||
|
if (combat != null) {
|
||||||
|
attackingFlyer = CardLists.getKeyword(combat.getAttackers(), Keyword.FLYING);
|
||||||
|
}
|
||||||
|
|
||||||
if (ph.isPlayerTurn(opp)
|
if (ph.isPlayerTurn(opp)
|
||||||
&& ph.getPhase() == PhaseType.COMBAT_DECLARE_ATTACKERS
|
&& ph.getPhase() == PhaseType.COMBAT_DECLARE_ATTACKERS
|
||||||
&& !CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty()
|
&& !attackingFlyer.isEmpty()
|
||||||
&& !card.hasKeyword("Reach")
|
&& !card.hasKeyword(Keyword.REACH)
|
||||||
&& CombatUtil.canBlock(card)
|
&& CombatUtil.canBlock(card)
|
||||||
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
|
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"));
|
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword(Keyword.FLYING), CardPredicates.hasKeyword(Keyword.REACH));
|
||||||
|
if (ph.isPlayerTurn(opp) && combat != null
|
||||||
|
&& !attackingFlyer.isEmpty()
|
||||||
|
&& 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 : attackingFlyer) {
|
||||||
|
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
|
||||||
@@ -217,7 +231,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
} else if (keyword.endsWith("Horsemanship")) {
|
} else if (keyword.endsWith("Horsemanship")) {
|
||||||
if (ph.isPlayerTurn(opp)
|
if (ph.isPlayerTurn(opp)
|
||||||
&& ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
&& ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||||
&& !CardLists.getKeyword(game.getCombat().getAttackers(), "Horsemanship").isEmpty()
|
&& !CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.HORSEMANSHIP).isEmpty()
|
||||||
&& CombatUtil.canBlock(card)
|
&& CombatUtil.canBlock(card)
|
||||||
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
|
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
|
||||||
return true;
|
return true;
|
||||||
@@ -226,7 +240,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||||
|| newPower <= 0
|
|| newPower <= 0
|
||||||
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
|
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
|
||||||
"Horsemanship").isEmpty()) {
|
Keyword.HORSEMANSHIP).isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.endsWith("Intimidate")) {
|
} else if (keyword.endsWith("Intimidate")) {
|
||||||
@@ -254,6 +268,13 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.endsWith("Indestructible")) {
|
} else if (keyword.endsWith("Indestructible")) {
|
||||||
|
// Predicting threatened objects in relevant non-combat situations happens elsewhere,
|
||||||
|
// so we are only worrying about combat relevance of Indestructible at this point.
|
||||||
|
if (combat == null
|
||||||
|
|| !((combat.isBlocked(card) || combat.isBlocking(card))
|
||||||
|
&& ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
} else if (keyword.endsWith("Deathtouch")) {
|
} else if (keyword.endsWith("Deathtouch")) {
|
||||||
if (ph.isPlayerTurn(opp) && ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
if (ph.isPlayerTurn(opp) && ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||||
@@ -283,7 +304,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("First Strike")) {
|
} else if (keyword.equals("First Strike")) {
|
||||||
if (card.hasKeyword("Double Strike")) {
|
if (card.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) {
|
if (combat != null && combat.isBlocked(card) && !combat.getBlockers(card).isEmpty()) {
|
||||||
@@ -323,7 +344,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
|| newPower <= 0
|
|| newPower <= 0
|
||||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||||
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
|
|| CardLists.getNotKeyword(CardLists.filter(opp.getCreaturesInPlay(), CardPredicates.possibleBlockers(card)),
|
||||||
"Flanking").isEmpty()) {
|
Keyword.FLANKING).isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.startsWith("Trample")) {
|
} else if (keyword.startsWith("Trample")) {
|
||||||
@@ -338,7 +359,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
if (newPower <= 0) {
|
if (newPower <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (combat != null && combat.isBlocking(card) && !card.hasKeyword("Wither")) {
|
if (combat != null && combat.isBlocking(card) && !card.hasKeyword(Keyword.WITHER)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if ((ph.isPlayerTurn(opp))
|
if ((ph.isPlayerTurn(opp))
|
||||||
@@ -347,7 +368,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.endsWith("Wither")) {
|
} else if (keyword.endsWith("Wither")) {
|
||||||
if (newPower <= 0 || card.hasKeyword("Infect")) {
|
if (newPower <= 0 || card.hasKeyword(Keyword.INFECT)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return combat != null && ( combat.isBlocking(card) || (combat.isAttacking(card) && combat.isBlocked(card)) );
|
return combat != null && ( combat.isBlocking(card) || (combat.isAttacking(card) && combat.isBlocked(card)) );
|
||||||
@@ -360,18 +381,18 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
if (ph.isPlayerTurn(opp) || !CombatUtil.canAttack(card, opp)
|
if (ph.isPlayerTurn(opp) || !CombatUtil.canAttack(card, opp)
|
||||||
|| newPower <= 0
|
|| newPower <= 0
|
||||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||||
|| CardLists.getNotKeyword(opp.getCreaturesInPlay(), "Defender").isEmpty()) {
|
|| CardLists.getNotKeyword(opp.getCreaturesInPlay(), Keyword.DEFENDER).isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("Reach")) {
|
} else if (keyword.equals("Reach")) {
|
||||||
if (ph.isPlayerTurn(ai)
|
if (ph.isPlayerTurn(ai)
|
||||||
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||||
|| CardLists.getKeyword(game.getCombat().getAttackers(), "Flying").isEmpty()
|
|| CardLists.getKeyword(game.getCombat().getAttackers(), Keyword.FLYING).isEmpty()
|
||||||
|| card.hasKeyword("Flying")
|
|| card.hasKeyword(Keyword.FLYING)
|
||||||
|| !CombatUtil.canBlock(card)) {
|
|| !CombatUtil.canBlock(card)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) {
|
} else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
|
||||||
if (ph.isPlayerTurn(ai)
|
if (ph.isPlayerTurn(ai)
|
||||||
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||||
return false;
|
return false;
|
||||||
@@ -394,7 +415,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("Persist")) {
|
} else if (keyword.equals("Persist")) {
|
||||||
if (card.getBaseToughness() <= 1 || card.hasKeyword("Undying")) {
|
if (card.getBaseToughness() <= 1 || card.hasKeyword(Keyword.UNDYING)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.equals("Islandwalk")) {
|
} else if (keyword.equals("Islandwalk")) {
|
||||||
@@ -430,11 +451,15 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
} else if (keyword.endsWith("CARDNAME can attack as though it didn't have defender.")) {
|
} else if (keyword.endsWith("CARDNAME can attack as though it didn't have defender.")) {
|
||||||
if (!ph.isPlayerTurn(ai) || !card.hasKeyword("Defender")
|
if (!ph.isPlayerTurn(ai) || !card.hasKeyword(Keyword.DEFENDER)
|
||||||
|| ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)
|
|| ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)
|
||||||
|| card.isTapped() || newPower <= 0) {
|
|| card.isTapped() || newPower <= 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
} else if (keyword.equals("Prevent all combat damage that would be dealt to CARDNAME.")) {
|
||||||
|
if (combat == null || !(combat.isBlocking(card) || combat.isBlocked(card))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -491,7 +516,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
if (c.getSVar("Targeting").equals("Dies") || c.getNetToughness() <= -defense) {
|
if (c.getSVar("Targeting").equals("Dies") || c.getNetToughness() <= -defense) {
|
||||||
return true; // can kill indestructible creatures
|
return true; // can kill indestructible creatures
|
||||||
}
|
}
|
||||||
return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword("Indestructible"));
|
return (ComputerUtilCombat.getDamageToKill(c) <= -defense && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
|
||||||
}
|
}
|
||||||
}); // leaves all creatures that will be destroyed
|
}); // leaves all creatures that will be destroyed
|
||||||
} // -X/-X end
|
} // -X/-X end
|
||||||
@@ -532,6 +557,15 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
|||||||
else {
|
else {
|
||||||
final boolean addsKeywords = !keywords.isEmpty();
|
final boolean addsKeywords = !keywords.isEmpty();
|
||||||
if (addsKeywords) {
|
if (addsKeywords) {
|
||||||
|
|
||||||
|
// If the keyword can prevent a creature from attacking, see if there's some kind of viable prioritization
|
||||||
|
if (keywords.contains("CARDNAME can't attack.") || keywords.contains("CARDNAME can't attack or block.")
|
||||||
|
|| keywords.contains("HIDDEN CARDNAME can't attack.") || keywords.contains("HIDDEN CARDNAME can't attack or block.")) {
|
||||||
|
if (CardLists.getNotType(list, "Creature").isEmpty()) {
|
||||||
|
list = ComputerUtilCard.prioritizeCreaturesWorthRemovingNow(ai, list, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(final Card c) {
|
public boolean apply(final Card c) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import forge.game.card.CardCollection;
|
|||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
import forge.game.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -48,7 +49,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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +86,7 @@ public class PumpAllAi extends PumpAiBase {
|
|||||||
if (c.getNetToughness() <= -defense) {
|
if (c.getNetToughness() <= -defense) {
|
||||||
return true; // can kill indestructible creatures
|
return true; // can kill indestructible creatures
|
||||||
}
|
}
|
||||||
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible"));
|
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
|
||||||
}
|
}
|
||||||
}); // leaves all creatures that will be destroyed
|
}); // leaves all creatures that will be destroyed
|
||||||
human = CardLists.filter(human, new Predicate<Card>() {
|
human = CardLists.filter(human, new Predicate<Card>() {
|
||||||
@@ -94,7 +95,7 @@ public class PumpAllAi extends PumpAiBase {
|
|||||||
if (c.getNetToughness() <= -defense) {
|
if (c.getNetToughness() <= -defense) {
|
||||||
return true; // can kill indestructible creatures
|
return true; // can kill indestructible creatures
|
||||||
}
|
}
|
||||||
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword("Indestructible"));
|
return ((ComputerUtilCombat.getDamageToKill(c) <= -defense) && !c.hasKeyword(Keyword.INDESTRUCTIBLE));
|
||||||
}
|
}
|
||||||
}); // leaves all creatures that will be destroyed
|
}); // leaves all creatures that will be destroyed
|
||||||
} // -X/-X end
|
} // -X/-X end
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ public class RemoveFromCombatAi extends SpellAbilityAi {
|
|||||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||||
// AI should only activate this during Human's turn
|
// AI should only activate this during Human's turn
|
||||||
|
|
||||||
|
if ("RemoveBestAttacker".equals(sa.getParam("AILogic"))) {
|
||||||
|
if (aiPlayer.getGame().getCombat() != null && aiPlayer.getGame().getCombat().getDefenders().contains(aiPlayer)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO - implement AI
|
// TODO - implement AI
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,9 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
|
||||||
import forge.ai.AiController;
|
import forge.ai.*;
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilMana;
|
|
||||||
import forge.ai.PlayerControllerAi;
|
|
||||||
import forge.ai.SpellAbilityAi;
|
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
|
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.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
@@ -28,7 +25,10 @@ public class RepeatAi extends SpellAbilityAi {
|
|||||||
sa.getTargets().add(opp);
|
sa.getTargets().add(opp);
|
||||||
}
|
}
|
||||||
String logic = sa.getParam("AILogic");
|
String logic = sa.getParam("AILogic");
|
||||||
if ("MaxX".equals(logic)) {
|
if ("MaxX".equals(logic) || "MaxXAtOppEOT".equals(logic)) {
|
||||||
|
if ("MaxXAtOppEOT".equals(logic) && !(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
// Set PayX here to maximum value.
|
// Set PayX here to maximum value.
|
||||||
final int max = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
final int max = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||||
source.setSVar("PayX", Integer.toString(max));
|
source.setSVar("PayX", Integer.toString(max));
|
||||||
@@ -60,10 +60,10 @@ public class RepeatAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// setup subability to repeat
|
// setup subability to repeat
|
||||||
final SpellAbility repeat = sa.getAdditonalAbility("RepeatSubAbility");
|
final SpellAbility repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||||
|
|
||||||
if (repeat == null) {
|
if (repeat == null) {
|
||||||
return false;
|
return mandatory;
|
||||||
}
|
}
|
||||||
|
|
||||||
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import com.google.common.base.Predicate;
|
import com.google.common.base.Predicate;
|
||||||
|
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
|
import forge.ai.SpecialCardAi;
|
||||||
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;
|
||||||
@@ -15,6 +13,9 @@ 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;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
|
||||||
public class RepeatEachAi extends SpellAbilityAi {
|
public class RepeatEachAi extends SpellAbilityAi {
|
||||||
@@ -26,7 +27,9 @@ public class RepeatEachAi extends SpellAbilityAi {
|
|||||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||||
String logic = sa.getParam("AILogic");
|
String logic = sa.getParam("AILogic");
|
||||||
|
|
||||||
if ("Never".equals(logic)) {
|
if ("PriceOfProgress".equals(logic)) {
|
||||||
|
return SpecialCardAi.PriceOfProgress.consider(aiPlayer, sa);
|
||||||
|
} else if ("Never".equals(logic)) {
|
||||||
return false;
|
return false;
|
||||||
} else if ("CloneMyTokens".equals(logic)) {
|
} else if ("CloneMyTokens".equals(logic)) {
|
||||||
if (CardLists.filter(aiPlayer.getCreaturesInPlay(), Presets.TOKEN).size() < 2) {
|
if (CardLists.filter(aiPlayer.getCreaturesInPlay(), Presets.TOKEN).size() < 2) {
|
||||||
@@ -81,11 +84,11 @@ public class RepeatEachAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
} else if ("AllPlayerLoseLife".equals(logic)) {
|
} else if ("AllPlayerLoseLife".equals(logic)) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
AbilitySub repeat = sa.getAdditonalAbility("RepeatSubAbility");
|
AbilitySub repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||||
|
|
||||||
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()) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
import forge.ai.AiPlayDecision;
|
import forge.ai.AiPlayDecision;
|
||||||
import forge.ai.PlayerControllerAi;
|
import forge.ai.PlayerControllerAi;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
@@ -23,8 +21,7 @@ public class RevealAi extends RevealAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
|
|
||||||
|
|
||||||
if (SpellAbilityAi.playReusable(ai, sa)) {
|
if (SpellAbilityAi.playReusable(ai, sa)) {
|
||||||
randomReturn = true;
|
randomReturn = true;
|
||||||
@@ -34,7 +31,7 @@ public class RevealAi extends RevealAiBase {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||||
// logic to see if it should reveal Mircacle Card
|
// logic to see if it should reveal Miracle Card
|
||||||
if (sa.hasParam("MiracleCost")) {
|
if (sa.hasParam("MiracleCost")) {
|
||||||
final Card c = sa.getHostCard();
|
final Card c = sa.getHostCard();
|
||||||
for (SpellAbility s : c.getBasicSpells()) {
|
for (SpellAbility s : c.getBasicSpells()) {
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
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;
|
||||||
@@ -20,8 +18,7 @@ public class RevealHandAi extends RevealAiBase {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.667, sa.getActivationsThisTurn() + 1);
|
|
||||||
|
|
||||||
if (SpellAbilityAi.playReusable(ai, sa)) {
|
if (SpellAbilityAi.playReusable(ai, sa)) {
|
||||||
randomReturn = true;
|
randomReturn = true;
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import forge.game.ability.AbilityUtils;
|
|||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
import forge.game.card.CardPredicates;
|
import forge.game.card.CardPredicates;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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;
|
||||||
@@ -82,7 +83,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
|||||||
if (!destroy) {
|
if (!destroy) {
|
||||||
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(sa));
|
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(sa));
|
||||||
} else {
|
} else {
|
||||||
if (!CardLists.getKeyword(list, "Indestructible").isEmpty()) {
|
if (!CardLists.getKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
|
||||||
// human can choose to destroy indestructibles
|
// human can choose to destroy indestructibles
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +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;
|
|
||||||
|
|
||||||
public class SacrificeAllAi extends SpellAbilityAi {
|
public class SacrificeAllAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -22,7 +21,6 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
|||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
// AI needs to be expanded, since this function can be pretty complex
|
// AI needs to be expanded, since this function can be pretty complex
|
||||||
// based on what the expected targets could be
|
// based on what the expected targets could be
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
final Cost abCost = sa.getPayCosts();
|
final Cost abCost = sa.getPayCosts();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
String valid = "";
|
String valid = "";
|
||||||
@@ -35,7 +33,7 @@ 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 =
|
||||||
@@ -51,7 +49,7 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
boolean chance = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
|
|
||||||
// if only creatures are affected evaluate both lists and pass only if
|
// if only creatures are affected evaluate both lists and pass only if
|
||||||
// human creatures are more valuable
|
// human creatures are more valuable
|
||||||
@@ -73,7 +71,7 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ((r.nextFloat() < .9667) && chance);
|
return ((MyRandom.getRandom().nextFloat() < .9667) && chance);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
|
import com.google.common.base.Predicates;
|
||||||
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.card.Card;
|
import forge.game.ability.ApiType;
|
||||||
|
import forge.game.card.*;
|
||||||
import forge.game.card.Card.SplitCMCMode;
|
import forge.game.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;
|
||||||
@@ -16,10 +15,6 @@ import forge.game.spellability.TargetRestrictions;
|
|||||||
import forge.game.zone.ZoneType;
|
import forge.game.zone.ZoneType;
|
||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
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,24 @@ 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AI logic to scry in Main 1 if there is no better option, otherwise scry at opponent's EOT
|
||||||
|
// (e.g. Glimmer of Genius)
|
||||||
|
if ("BestOpportunity".equals(sa.getParam("AILogic"))) {
|
||||||
|
return doBestOpportunityLogic(ai, sa, ph);
|
||||||
|
}
|
||||||
|
|
||||||
// in the playerturn Scry should only be done in Main1 or in upkeep if able
|
// 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)) {
|
||||||
@@ -60,6 +73,27 @@ public class ScryAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean doBestOpportunityLogic(Player ai, SpellAbility sa, PhaseHandler ph) {
|
||||||
|
// Check to see if there are any cards in hand that may be worth casting
|
||||||
|
boolean hasSomethingElse = false;
|
||||||
|
for (Card c : CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS))) {
|
||||||
|
for (SpellAbility ab : c.getAllSpellAbilities()) {
|
||||||
|
if (ab.getPayCosts() != null
|
||||||
|
&& ab.getPayCosts().hasManaCost()
|
||||||
|
&& ComputerUtilMana.hasEnoughManaSourcesToCast(ab, ai)) {
|
||||||
|
// TODO: currently looks for non-Scry cards, can most certainly be made smarter.
|
||||||
|
if (ab.getApi() != ApiType.Scry) {
|
||||||
|
hasSomethingElse = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (!hasSomethingElse && ph.getPlayerTurn() == ai && ph.getPhase().isAfter(PhaseType.DRAW))
|
||||||
|
|| (ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
* Checks if the AI will play a SpellAbility with the specified AiLogic
|
||||||
*/
|
*/
|
||||||
@@ -135,8 +169,7 @@ public class ScryAi extends SpellAbilityAi {
|
|||||||
chance = .667; // 66.7% chance for sorcery speed (since it will
|
chance = .667; // 66.7% chance for sorcery speed (since it will
|
||||||
// never activate EOT)
|
// never activate EOT)
|
||||||
}
|
}
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
|
||||||
|
|
||||||
if (SpellAbilityAi.playReusable(ai, sa)) {
|
if (SpellAbilityAi.playReusable(ai, sa)) {
|
||||||
randomReturn = true;
|
randomReturn = true;
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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.CardStateName;
|
import forge.card.CardStateName;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
import forge.game.GlobalRuleChange;
|
import forge.game.GlobalRuleChange;
|
||||||
@@ -13,6 +12,7 @@ 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;
|
||||||
@@ -62,6 +62,11 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
|
||||||
|
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
|
||||||
@@ -73,29 +78,28 @@ 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)) {
|
||||||
if (!sa.usesTargeting()) {
|
if (!sa.usesTargeting()) {
|
||||||
// no Transform with Defined which is not Self
|
// no Transform with Defined which is not Self
|
||||||
if (source.hasKeyword("CARDNAME can't transform")) {
|
if (!source.canTransform()) {
|
||||||
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();
|
||||||
|
|
||||||
CardCollection list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
|
CardCollection list = CardLists.getValidCards(CardLists.filter(game.getCardsIn(ZoneType.Battlefield), Presets.CREATURES), tgt.getValidTgts(), ai, source, sa);
|
||||||
// select only cards with Transform as SplitType
|
// select only the ones that can transform
|
||||||
list = CardLists.filter(list, new Predicate<Card>() {
|
list = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@Override
|
@Override
|
||||||
public boolean apply(Card c) {
|
public boolean apply(Card c) {
|
||||||
return c.hasAlternateState() && c.getRules().getSplitType() == CardSplitType.Transform;
|
return c.canTransform();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// select only the ones that can transform
|
|
||||||
list = CardLists.getNotKeyword(list, "CARDNAME can't transform");
|
|
||||||
list = CardLists.getTargetableCards(list, sa);
|
list = CardLists.getTargetableCards(list, sa);
|
||||||
|
|
||||||
if (list.isEmpty()) {
|
if (list.isEmpty()) {
|
||||||
@@ -103,7 +107,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;
|
||||||
@@ -121,7 +125,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();
|
||||||
@@ -134,7 +138,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;
|
||||||
@@ -158,9 +162,11 @@ public class SetStateAi extends SpellAbilityAi {
|
|||||||
|
|
||||||
// need a copy for evaluation
|
// need a copy for evaluation
|
||||||
Card transformed = CardUtil.getLKICopy(card);
|
Card transformed = CardUtil.getLKICopy(card);
|
||||||
transformed.getCurrentState().copyFrom(card, card.getAlternateState());
|
transformed.getCurrentState().copyFrom(card.getAlternateState(), true);
|
||||||
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);
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -248,4 +254,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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
|
||||||
import forge.ai.ComputerUtilAbility;
|
|
||||||
import forge.ai.ComputerUtilCombat;
|
|
||||||
import forge.ai.ComputerUtilMana;
|
import forge.ai.ComputerUtilMana;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
@@ -25,7 +22,6 @@ public class StoreSVarAi extends SpellAbilityAi {
|
|||||||
@Override
|
@Override
|
||||||
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 String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
|
||||||
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();
|
||||||
@@ -69,62 +65,7 @@ public class StoreSVarAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
else if ("Tree of Redemption".equals(sourceName)) {
|
|
||||||
if (!ai.canGainLife())
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// someone controls "Rain of Gore" or "Sulfuric Vortex", lifegain is bad in that case
|
|
||||||
if (game.isCardInPlay("Rain of Gore") || game.isCardInPlay("Sulfuric Vortex"))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
// an opponent controls "Tainted Remedy", lifegain is bad in that case
|
|
||||||
for (Player op : ai.getOpponents()) {
|
|
||||||
if (op.isCardInPlay("Tainted Remedy"))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ComputerUtil.waitForBlocking(sa) || ai.getLife() + 1 >= source.getNetToughness()
|
|
||||||
|| (ai.getLife() > 5 && !ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if ("Tree of Perdition".equals(sourceName)) {
|
|
||||||
boolean shouldDo = false;
|
|
||||||
|
|
||||||
if (ComputerUtil.waitForBlocking(sa))
|
|
||||||
return false;
|
|
||||||
|
|
||||||
for (Player op : ai.getOpponents()) {
|
|
||||||
// if oppoent can't be targeted, or it can't lose life, try another one
|
|
||||||
if (!op.canBeTargetedBy(sa) || !op.canLoseLife())
|
|
||||||
continue;
|
|
||||||
// an opponent has more live than this toughness
|
|
||||||
if (op.getLife() + 1 >= source.getNetToughness()) {
|
|
||||||
shouldDo = true;
|
|
||||||
} else {
|
|
||||||
// opponent can't gain life, so "Tainted Remedy" should not work.
|
|
||||||
if (!op.canGainLife()) {
|
|
||||||
continue;
|
|
||||||
} else if (ai.isCardInPlay("Tainted Remedy")) { // or AI has Tainted Remedy
|
|
||||||
shouldDo = true;
|
|
||||||
} else {
|
|
||||||
for (Player ally : ai.getAllies()) {
|
|
||||||
// if an Ally has Tainted Remedy and opponent is also opponent of ally
|
|
||||||
if (ally.isCardInPlay("Tainted Remedy") && op.isOpponentOf(ally))
|
|
||||||
shouldDo = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (shouldDo) {
|
|
||||||
sa.getTargets().add(op);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return shouldDo;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
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;
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ 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.ComputerUtil;
|
||||||
|
import forge.ai.ComputerUtilAbility;
|
||||||
import forge.ai.ComputerUtilCard;
|
import forge.ai.ComputerUtilCard;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.ai.SpellAbilityAi;
|
||||||
import forge.game.Game;
|
import forge.game.Game;
|
||||||
|
import forge.game.ability.ApiType;
|
||||||
import forge.game.card.Card;
|
import forge.game.card.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardLists;
|
import forge.game.card.CardLists;
|
||||||
@@ -155,6 +156,11 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//try to exclude things that will already be tapped due to something on stack or because something is
|
||||||
|
//already targeted in a parent or sub SA
|
||||||
|
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, tapList, sa, ApiType.Tap);
|
||||||
|
tapList.removeAll(toExclude);
|
||||||
|
|
||||||
if (tapList.isEmpty()) {
|
if (tapList.isEmpty()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import forge.game.zone.ZoneType;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class TapAllAi extends SpellAbilityAi {
|
public class TapAllAi extends SpellAbilityAi {
|
||||||
@Override
|
@Override
|
||||||
@@ -64,8 +63,7 @@ public class TapAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
if (MyRandom.getRandom().nextFloat() > Math.pow(.6667, sa.getActivationsThisTurn())) {
|
||||||
if (r.nextFloat() > Math.pow(.6667, sa.getActivationsThisTurn())) {
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -135,9 +133,8 @@ public class TapAllAi extends SpellAbilityAi {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
boolean rr = false;
|
boolean rr = false;
|
||||||
if (r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
|
if (MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn())) {
|
||||||
rr = true;
|
rr = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import forge.game.spellability.TargetRestrictions;
|
|||||||
import forge.util.MyRandom;
|
import forge.util.MyRandom;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class TapOrUntapAi extends TapAiBase {
|
public class TapOrUntapAi extends TapAiBase {
|
||||||
|
|
||||||
@@ -20,8 +19,7 @@ public class TapOrUntapAi extends TapAiBase {
|
|||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
final Random r = MyRandom.getRandom();
|
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||||
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
|
||||||
|
|
||||||
if (tgt == null) {
|
if (tgt == null) {
|
||||||
// assume we are looking to tap human's stuff
|
// assume we are looking to tap human's stuff
|
||||||
|
|||||||
@@ -1,31 +1,20 @@
|
|||||||
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.token.TokenInfo;
|
||||||
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;
|
||||||
import forge.game.cost.CostRemoveCounter;
|
import forge.game.cost.CostRemoveCounter;
|
||||||
|
import forge.game.keyword.Keyword;
|
||||||
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 +27,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>
|
||||||
@@ -208,7 +202,8 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
sa.getTargets().add(ai);
|
sa.getTargets().add(ai);
|
||||||
} else {
|
} else {
|
||||||
// Flash Foliage
|
// Flash Foliage
|
||||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield),
|
||||||
|
ai.getOpponents());
|
||||||
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
||||||
list = CardLists.getTargetableCards(list, sa);
|
list = CardLists.getTargetableCards(list, sa);
|
||||||
CardCollection betterList = CardLists.filter(list, new Predicate<Card>() {
|
CardCollection betterList = CardLists.filter(list, new Predicate<Card>() {
|
||||||
@@ -220,7 +215,7 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
if (!betterList.isEmpty()) {
|
if (!betterList.isEmpty()) {
|
||||||
list = betterList;
|
list = betterList;
|
||||||
}
|
}
|
||||||
betterList = CardLists.getNotKeyword(list, "Trample");
|
betterList = CardLists.getNotKeyword(list, Keyword.TRAMPLE);
|
||||||
if (!betterList.isEmpty()) {
|
if (!betterList.isEmpty()) {
|
||||||
list = betterList;
|
list = betterList;
|
||||||
}
|
}
|
||||||
@@ -229,11 +224,32 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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() < .8;
|
return MyRandom.getRandom().nextFloat() <= chance;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -414,7 +430,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);
|
||||||
}
|
}
|
||||||
@@ -436,9 +452,9 @@ public class TokenAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
final String substitutedName = tokenName.equals("ChosenType") ? host.getChosenType() : tokenName;
|
final String substitutedName = tokenName.equals("ChosenType") ? host.getChosenType() : tokenName;
|
||||||
final String imageName = imageNames.get(MyRandom.getRandom().nextInt(imageNames.size()));
|
final String imageName = imageNames.get(MyRandom.getRandom().nextInt(imageNames.size()));
|
||||||
final CardFactory.TokenInfo tokenInfo = new CardFactory.TokenInfo(substitutedName, imageName,
|
final TokenInfo tokenInfo = new TokenInfo(substitutedName, imageName,
|
||||||
cost, substitutedTypes, tokenKeywords, finalPower, finalToughness);
|
cost, substitutedTypes, tokenKeywords, finalPower, finalToughness);
|
||||||
Card token = CardFactory.makeOneToken(tokenInfo, ai);
|
Card token = tokenInfo.makeOneToken(ai);
|
||||||
|
|
||||||
if (token == null) {
|
if (token == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import forge.util.MyRandom;
|
|||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
|
||||||
|
|
||||||
public class UnattachAllAi extends SpellAbilityAi {
|
public class UnattachAllAi extends SpellAbilityAi {
|
||||||
|
|
||||||
@@ -25,7 +24,6 @@ public class UnattachAllAi extends SpellAbilityAi {
|
|||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||||
final Random r = MyRandom.getRandom();
|
|
||||||
final Cost abCost = sa.getPayCosts();
|
final Cost abCost = sa.getPayCosts();
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
|
|
||||||
@@ -34,7 +32,7 @@ public class UnattachAllAi extends SpellAbilityAi {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// prevent run-away activations - first time will always return true
|
// prevent run-away activations - first time will always return true
|
||||||
boolean chance = r.nextFloat() <= .9;
|
boolean chance = MyRandom.getRandom().nextFloat() <= .9;
|
||||||
|
|
||||||
// Attach spells always have a target
|
// Attach spells always have a target
|
||||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||||
|
|||||||
@@ -1,17 +1,21 @@
|
|||||||
package forge.ai.ability;
|
package forge.ai.ability;
|
||||||
|
|
||||||
import forge.ai.ComputerUtil;
|
import com.google.common.base.Predicate;
|
||||||
import forge.ai.ComputerUtilCard;
|
import com.google.common.base.Predicates;
|
||||||
import forge.ai.ComputerUtilCost;
|
import forge.ai.*;
|
||||||
import forge.ai.SpellAbilityAi;
|
import forge.card.mana.ManaCostShard;
|
||||||
|
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.Card;
|
||||||
import forge.game.card.CardCollection;
|
import forge.game.card.CardCollection;
|
||||||
import forge.game.card.CardLists;
|
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.cost.Cost;
|
import forge.game.cost.Cost;
|
||||||
import forge.game.cost.CostTap;
|
import forge.game.cost.CostTap;
|
||||||
|
import forge.game.mana.ManaCostBeingPaid;
|
||||||
|
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.PlayerCollection;
|
import forge.game.player.PlayerCollection;
|
||||||
@@ -25,9 +29,11 @@ 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) {
|
||||||
final Card source = sa.getHostCard();
|
final Card source = sa.getHostCard();
|
||||||
if ("EOT".equals(sa.getParam("AILogic")) && (source.getGame().getPhaseHandler().getNextTurn() != ai
|
if ("EOT".equals(aiLogic) && (source.getGame().getPhaseHandler().getNextTurn() != ai
|
||||||
|| !source.getGame().getPhaseHandler().getPhase().equals(PhaseType.END_OF_TURN))) {
|
|| !source.getGame().getPhaseHandler().getPhase().equals(PhaseType.END_OF_TURN))) {
|
||||||
return false;
|
return false;
|
||||||
|
} else if ("PoolExtraMana".equals(aiLogic)) {
|
||||||
|
return doPoolExtraManaLogic(ai, sa);
|
||||||
}
|
}
|
||||||
|
|
||||||
return !("Never".equals(aiLogic));
|
return !("Never".equals(aiLogic));
|
||||||
@@ -143,7 +149,16 @@ public class UntapAi extends SpellAbilityAi {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
CardCollection untapList = CardLists.filter(list, Presets.TAPPED);
|
// For some abilities, it may be worth to target even an untapped card if we're targeting mostly for the subability
|
||||||
|
boolean targetUntapped = false;
|
||||||
|
if (sa.getSubAbility() != null) {
|
||||||
|
SpellAbility subSa = sa.getSubAbility();
|
||||||
|
if (subSa.getApi() == ApiType.RemoveFromCombat && "RemoveBestAttacker".equals(subSa.getParam("AILogic"))) {
|
||||||
|
targetUntapped = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CardCollection untapList = targetUntapped ? list : CardLists.filter(list, Presets.TAPPED);
|
||||||
// filter out enchantments and planeswalkers, their tapped state doesn't
|
// filter out enchantments and planeswalkers, their tapped state doesn't
|
||||||
// matter.
|
// matter.
|
||||||
final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
|
final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
|
||||||
@@ -167,12 +182,17 @@ public class UntapAi extends SpellAbilityAi {
|
|||||||
untapList.removeAll(toRemove);
|
untapList.removeAll(toRemove);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//try to exclude things that will already be untapped due to something on stack or because something is
|
||||||
|
//already targeted in a parent or sub SA
|
||||||
|
CardCollection toExclude = ComputerUtilAbility.getCardsTargetedWithApi(ai, untapList, sa, ApiType.Untap);
|
||||||
|
untapList.removeAll(toExclude);
|
||||||
|
|
||||||
sa.resetTargets();
|
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;
|
||||||
|
|
||||||
if (untapList.isEmpty()) {
|
if (untapList.isEmpty()) {
|
||||||
// Animate untapped lands (Koth of the Hamer)
|
// Animate untapped lands (Koth of the Hammer)
|
||||||
if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.Animate && !list.isEmpty()
|
if (sa.getSubAbility() != null && sa.getSubAbility().getApi() == ApiType.Animate && !list.isEmpty()
|
||||||
&& ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
&& ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
|
||||||
choice = ComputerUtilCard.getWorstPermanentAI(list, false, false, false, false);
|
choice = ComputerUtilCard.getWorstPermanentAI(list, false, false, false, false);
|
||||||
@@ -184,17 +204,13 @@ public class UntapAi extends SpellAbilityAi {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
//Untap Time Vault? - Yes please!
|
choice = detectPriorityUntapTargets(untapList);
|
||||||
for (Card c : untapList) {
|
|
||||||
if (c.getName().equals("Time Vault")) {
|
|
||||||
choice = c;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (choice == null) {
|
if (choice == null) {
|
||||||
if (CardLists.getNotType(untapList, "Creature").isEmpty()) {
|
if (CardLists.getNotType(untapList, "Creature").isEmpty()) {
|
||||||
choice = ComputerUtilCard.getBestCreatureAI(untapList); // if only creatures take the best
|
choice = ComputerUtilCard.getBestCreatureAI(untapList); // if only creatures take the best
|
||||||
} else if (!sa.getPayCosts().hasManaCost() || sa.getRootAbility().isTrigger()) {
|
} else if (!sa.getPayCosts().hasManaCost() || sa.getRootAbility().isTrigger()
|
||||||
|
|| "Always".equals(sa.getParam("AILogic"))) {
|
||||||
choice = ComputerUtilCard.getMostExpensivePermanentAI(untapList, sa, false);
|
choice = ComputerUtilCard.getMostExpensivePermanentAI(untapList, sa, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,4 +335,116 @@ public class UntapAi extends SpellAbilityAi {
|
|||||||
pl.addAll(ai.getAllies());
|
pl.addAll(ai.getAllies());
|
||||||
return ComputerUtilCard.getBestAI(CardLists.filterControlledBy(list, pl));
|
return ComputerUtilCard.getBestAI(CardLists.filterControlledBy(list, pl));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static Card detectPriorityUntapTargets(final List<Card> untapList) {
|
||||||
|
// untap Time Vault or another broken card? - Yes please!
|
||||||
|
String[] priorityList = {"Time Vault", "Mana Vault", "Icy Manipulator", "Steel Overseer", "Grindclock", "Prototype Portal"};
|
||||||
|
for (String name : priorityList) {
|
||||||
|
for (Card c : untapList) {
|
||||||
|
if (c.getName().equals(name)) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// See if there's anything to untap that is tapped and that doesn't untap during the next untap step by itself
|
||||||
|
CardCollection noAutoUntap = CardLists.filter(untapList, CardPredicates.hasKeyword("CARDNAME doesn't untap during your untap step."));
|
||||||
|
if (!noAutoUntap.isEmpty()) {
|
||||||
|
return ComputerUtilCard.getBestAI(noAutoUntap);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean doPoolExtraManaLogic(final Player ai, final SpellAbility sa) {
|
||||||
|
final Card source = sa.getHostCard();
|
||||||
|
final PhaseHandler ph = source.getGame().getPhaseHandler();
|
||||||
|
final Game game = ai.getGame();
|
||||||
|
|
||||||
|
if (sa.getHostCard().isTapped()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if something is playable if we untap for an additional mana with this, then proceed
|
||||||
|
CardCollection inHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.not(CardPredicates.Presets.LANDS));
|
||||||
|
// The AI is not very good at timing non-permanent spells this way, so filter them out
|
||||||
|
// (it may actually be possible to enable this for sorceries, but that'll need some canPlay shenanigans)
|
||||||
|
CardCollection playable = CardLists.filter(inHand, Presets.PERMANENTS);
|
||||||
|
|
||||||
|
CardCollection untappingCards = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
boolean hasUntapLandLogic = false;
|
||||||
|
for (SpellAbility sa : card.getSpellAbilities()) {
|
||||||
|
if ("PoolExtraMana".equals(sa.getParam("AILogic"))) {
|
||||||
|
hasUntapLandLogic = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return hasUntapLandLogic && card.isUntapped();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// TODO: currently limited to Main 2, somehow improve to let the AI use this SA at other time?
|
||||||
|
if (ph.is(PhaseType.MAIN2, ai)) {
|
||||||
|
for (Card c : playable) {
|
||||||
|
for (SpellAbility ab : c.getBasicSpells()) {
|
||||||
|
if (!ComputerUtilMana.hasEnoughManaSourcesToCast(ab, ai)) {
|
||||||
|
// TODO: Currently limited to predicting something that can be paid with any color,
|
||||||
|
// can ideally be improved to work by color.
|
||||||
|
ManaCostBeingPaid reduced = new ManaCostBeingPaid(ab.getPayCosts().getCostMana().getManaCostFor(ab), ab.getPayCosts().getCostMana().getRestiction());
|
||||||
|
reduced.decreaseShard(ManaCostShard.GENERIC, untappingCards.size());
|
||||||
|
if (ComputerUtilMana.canPayManaCost(reduced, ab, ai)) {
|
||||||
|
CardCollection manaLandsTapped = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||||
|
Predicates.and(Presets.LANDS_PRODUCING_MANA, Presets.TAPPED));
|
||||||
|
manaLandsTapped = CardLists.filter(manaLandsTapped, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return card.isValid(sa.getParam("ValidTgts"), ai, source, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!manaLandsTapped.isEmpty()) {
|
||||||
|
// already have a tapped land, so agree to proceed with untapping it
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pool one additional mana by tapping a land to try to ramp to something
|
||||||
|
CardCollection manaLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
|
||||||
|
Predicates.and(Presets.LANDS_PRODUCING_MANA, Presets.UNTAPPED));
|
||||||
|
manaLands = CardLists.filter(manaLands, new Predicate<Card>() {
|
||||||
|
@Override
|
||||||
|
public boolean apply(Card card) {
|
||||||
|
return card.isValid(sa.getParam("ValidTgts"), ai, source, null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (manaLands.isEmpty()) {
|
||||||
|
// nothing to untap
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Card landToPool = manaLands.getFirst();
|
||||||
|
SpellAbility manaAb = landToPool.getManaAbilities().getFirst();
|
||||||
|
|
||||||
|
ComputerUtil.playNoStack(ai, manaAb, game);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// no harm in doing this past declare blockers during the opponent's turn and right before our turn,
|
||||||
|
// maybe we'll serendipitously untap into something like a removal spell or burn spell that'll help
|
||||||
|
if (ph.getNextTurn() == ai
|
||||||
|
&& (ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// haven't found any immediate playable options
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,12 +22,13 @@ import forge.game.card.Card;
|
|||||||
import forge.game.card.CardFactory;
|
import forge.game.card.CardFactory;
|
||||||
import forge.game.card.CardFactoryUtil;
|
import forge.game.card.CardFactoryUtil;
|
||||||
import forge.game.card.CounterType;
|
import forge.game.card.CounterType;
|
||||||
|
import forge.game.card.token.TokenInfo;
|
||||||
import forge.game.combat.Combat;
|
import forge.game.combat.Combat;
|
||||||
|
import forge.game.keyword.KeywordInterface;
|
||||||
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.RegisteredPlayer;
|
import forge.game.player.RegisteredPlayer;
|
||||||
import forge.game.spellability.AbilityActivated;
|
|
||||||
import forge.game.spellability.SpellAbility;
|
import forge.game.spellability.SpellAbility;
|
||||||
import forge.game.spellability.SpellAbilityRestriction;
|
import forge.game.spellability.SpellAbilityRestriction;
|
||||||
import forge.game.spellability.SpellAbilityStackInstance;
|
import forge.game.spellability.SpellAbilityStackInstance;
|
||||||
@@ -96,7 +97,7 @@ public class GameCopier {
|
|||||||
|
|
||||||
PhaseHandler origPhaseHandler = origGame.getPhaseHandler();
|
PhaseHandler origPhaseHandler = origGame.getPhaseHandler();
|
||||||
Player newPlayerTurn = playerMap.get(origPhaseHandler.getPlayerTurn());
|
Player newPlayerTurn = playerMap.get(origPhaseHandler.getPlayerTurn());
|
||||||
newGame.getPhaseHandler().devModeSet(origPhaseHandler.getPhase(), newPlayerTurn);
|
newGame.getPhaseHandler().devModeSet(origPhaseHandler.getPhase(), newPlayerTurn, origPhaseHandler.getTurn());
|
||||||
newGame.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
|
newGame.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
|
||||||
for (Player p : newGame.getPlayers()) {
|
for (Player p : newGame.getPlayers()) {
|
||||||
((PlayerZoneBattlefield) p.getZone(ZoneType.Battlefield)).setTriggers(false);
|
((PlayerZoneBattlefield) p.getZone(ZoneType.Battlefield)).setTriggers(false);
|
||||||
@@ -236,10 +237,8 @@ public class GameCopier {
|
|||||||
private static final boolean USE_FROM_PAPER_CARD = true;
|
private static final boolean USE_FROM_PAPER_CARD = true;
|
||||||
private Card createCardCopy(Game newGame, Player newOwner, Card c) {
|
private Card createCardCopy(Game newGame, Player newOwner, Card c) {
|
||||||
if (c.isToken() && !c.isEmblem()) {
|
if (c.isToken() && !c.isEmblem()) {
|
||||||
String tokenStr = new CardFactory.TokenInfo(c).toString();
|
Card result = new TokenInfo(c).makeOneToken(newOwner);
|
||||||
Card result = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), newOwner);
|
|
||||||
CardFactory.copyCopiableCharacteristics(c, result);
|
CardFactory.copyCopiableCharacteristics(c, result);
|
||||||
CardFactory.copyCopiableAbilities(c, result);
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
if (USE_FROM_PAPER_CARD && !c.isEmblem()) {
|
if (USE_FROM_PAPER_CARD && !c.isEmblem()) {
|
||||||
@@ -263,15 +262,8 @@ public class GameCopier {
|
|||||||
newCard.addStaticAbility(stAb);
|
newCard.addStaticAbility(stAb);
|
||||||
}
|
}
|
||||||
for (SpellAbility sa : c.getSpellAbilities()) {
|
for (SpellAbility sa : c.getSpellAbilities()) {
|
||||||
SpellAbility saCopy;
|
SpellAbility saCopy = sa.copy(newCard, true);
|
||||||
|
|
||||||
if (sa instanceof AbilityActivated) {
|
|
||||||
saCopy = ((AbilityActivated)sa).getCopy();
|
|
||||||
} else {
|
|
||||||
saCopy = sa.copy();
|
|
||||||
}
|
|
||||||
if (saCopy != null) {
|
if (saCopy != null) {
|
||||||
saCopy.setHostCard(newCard);
|
|
||||||
newCard.addSpellAbility(saCopy);
|
newCard.addSpellAbility(saCopy);
|
||||||
} else {
|
} else {
|
||||||
System.err.println(sa.toString());
|
System.err.println(sa.toString());
|
||||||
@@ -306,7 +298,7 @@ public class GameCopier {
|
|||||||
newCard.setChangedCardTypes(c.getChangedCardTypesMap());
|
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 (KeywordInterface kw : c.getHiddenExtrinsicKeywords())
|
||||||
newCard.addHiddenExtrinsicKeyword(kw);
|
newCard.addHiddenExtrinsicKeyword(kw);
|
||||||
newCard.setExtrinsicKeyword(Lists.newArrayList(c.getExtrinsicKeyword()));
|
newCard.setExtrinsicKeyword(Lists.newArrayList(c.getExtrinsicKeyword()));
|
||||||
if (c.isTapped()) {
|
if (c.isTapped()) {
|
||||||
@@ -326,7 +318,6 @@ public class GameCopier {
|
|||||||
}
|
}
|
||||||
if (c.isMonstrous()) {
|
if (c.isMonstrous()) {
|
||||||
newCard.setMonstrous(true);
|
newCard.setMonstrous(true);
|
||||||
newCard.setMonstrosityNum(c.getMonstrosityNum());
|
|
||||||
}
|
}
|
||||||
if (c.isRenowned()) {
|
if (c.isRenowned()) {
|
||||||
newCard.setRenowned(true);
|
newCard.setRenowned(true);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user