mirror of
https://github.com/Card-Forge/forge.git
synced 2025-11-14 01:38:13 +00:00
Compare commits
2218 Commits
exiledWith
...
forge-1.6.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d658df35d6 | ||
|
|
91ae1036de | ||
|
|
844098a302 | ||
|
|
08454489b4 | ||
|
|
71110568fc | ||
|
|
9990f0ad1a | ||
|
|
735a966312 | ||
|
|
828d4e17c9 | ||
|
|
233c1f6d00 | ||
|
|
dbe1683ba9 | ||
|
|
a5b7f93971 | ||
|
|
d853ca6615 | ||
|
|
418d2bf012 | ||
|
|
030f1f4056 | ||
|
|
fc82dbb9aa | ||
|
|
6feb18c11c | ||
|
|
c07f3ea34d | ||
|
|
8948210e0a | ||
|
|
df0b5362c2 | ||
|
|
a9b60a87e0 | ||
|
|
694317a50f | ||
|
|
a856834693 | ||
|
|
35b71da140 | ||
|
|
3b2e39808e | ||
|
|
697a2ace20 | ||
|
|
c38c53d513 | ||
|
|
31745fe31e | ||
|
|
f8e3f4dd31 | ||
|
|
965ec4bc47 | ||
|
|
9cf28e7041 | ||
|
|
71f639d45e | ||
|
|
084476b8b0 | ||
|
|
f9abb8ca75 | ||
|
|
4b0c748eaf | ||
|
|
083674071b | ||
|
|
96ccf0afc6 | ||
|
|
cfb755ce20 | ||
|
|
cc8753b563 | ||
|
|
454c51f1b1 | ||
|
|
fa83326710 | ||
|
|
eb60690973 | ||
|
|
0983c72ea0 | ||
|
|
49436a00b3 | ||
|
|
8345bf890b | ||
|
|
5b2980562b | ||
|
|
b86249635f | ||
|
|
4286429402 | ||
|
|
6578fb79e6 | ||
|
|
f1169511ab | ||
|
|
6f5f04bb7e | ||
|
|
b33595d840 | ||
|
|
653b484674 | ||
|
|
5a0a63940d | ||
|
|
d734fdf761 | ||
|
|
3ab7fcf224 | ||
|
|
4f4fe04683 | ||
|
|
c83d03a2d6 | ||
|
|
8ffa95a26a | ||
|
|
da96b580a4 | ||
|
|
5a862ab742 | ||
|
|
7e6f077b0a | ||
|
|
b6676c9d58 | ||
|
|
37099156f3 | ||
|
|
018a2c080b | ||
|
|
890ef37b3f | ||
|
|
c7de3866c7 | ||
|
|
4cf11743f2 | ||
|
|
3ee3a28fd5 | ||
|
|
39ba048d07 | ||
|
|
40a9229681 | ||
|
|
8a7873d0c9 | ||
|
|
996ddb4360 | ||
|
|
eec11c6ec5 | ||
|
|
197cbb1808 | ||
|
|
32df7f3eae | ||
|
|
4b3839b143 | ||
|
|
79216eaa8d | ||
|
|
ba2998541d | ||
|
|
d5aaa4dc45 | ||
|
|
999e4f81ee | ||
|
|
c38d2f531c | ||
|
|
27d8af3551 | ||
|
|
b10ce1b884 | ||
|
|
18c207f39b | ||
|
|
898df454d5 | ||
|
|
577c5ca055 | ||
|
|
04558e93b3 | ||
|
|
6b0a88cd95 | ||
|
|
bd125df278 | ||
|
|
fc31881150 | ||
|
|
9a13b06452 | ||
|
|
01e3a726fe | ||
|
|
bcaa318941 | ||
|
|
c64e5c2d5e | ||
|
|
caafcf61aa | ||
|
|
e6280377ce | ||
|
|
a2e66550bf | ||
|
|
abad45f874 | ||
|
|
fa649c1cd0 | ||
|
|
96ad12a1b7 | ||
|
|
669979531c | ||
|
|
0c81f5c36c | ||
|
|
7dfc48744c | ||
|
|
943d95eed0 | ||
|
|
4b6e5e32d1 | ||
|
|
a1132fbe90 | ||
|
|
ea37370738 | ||
|
|
3b73f86e1b | ||
|
|
1633dedee3 | ||
|
|
9ae41c67a2 | ||
|
|
58819f48bc | ||
|
|
1f2cc92533 | ||
|
|
2e550b95ca | ||
|
|
6f27a0db4d | ||
|
|
ace7998c40 | ||
|
|
969d65bbb2 | ||
|
|
dd3c6abdd2 | ||
|
|
45c50c1f6e | ||
|
|
93d284dc82 | ||
|
|
9f6269c3e8 | ||
|
|
54ed9cfe22 | ||
|
|
efe0b7ace0 | ||
|
|
96eeac14ea | ||
|
|
85409f1616 | ||
|
|
f7f020cc26 | ||
|
|
889173a682 | ||
|
|
e2fab353fd | ||
|
|
41a0769cca | ||
|
|
f73fac0eb4 | ||
|
|
3047e40116 | ||
|
|
24c45c7cc2 | ||
|
|
88b2ce84d6 | ||
|
|
9c55543f1b | ||
|
|
76eac28192 | ||
|
|
b3d2c28f06 | ||
|
|
a4c0a56734 | ||
|
|
fc78637d2f | ||
|
|
7605706b89 | ||
|
|
862f3e4e9d | ||
|
|
64de8327a6 | ||
|
|
c8d5624600 | ||
|
|
abe2d7d10a | ||
|
|
27dbddac94 | ||
|
|
cfd18eef5c | ||
|
|
e2b30b8f71 | ||
|
|
0f86978d8a | ||
|
|
1e7c21bdcf | ||
|
|
9e8fa209d5 | ||
|
|
83caef1a97 | ||
|
|
056daed06b | ||
|
|
ab31c8fa9d | ||
|
|
f95fb7d08b | ||
|
|
b2939f511d | ||
|
|
e1a08629a4 | ||
|
|
9c5eba7372 | ||
|
|
97ecf45da3 | ||
|
|
cf4321492e | ||
|
|
d5f996d249 | ||
|
|
dc65f639c7 | ||
|
|
64aa9fe0ae | ||
|
|
73f65505b0 | ||
|
|
49dc938eb3 | ||
|
|
687b547b47 | ||
|
|
4b85ef2a99 | ||
|
|
be7ef159cd | ||
|
|
8b6f0fb8eb | ||
|
|
ef29c61fb0 | ||
|
|
77ba81d621 | ||
|
|
4d645f761c | ||
|
|
b0aeaaabb4 | ||
|
|
80661e1646 | ||
|
|
f53e4495de | ||
|
|
76ee1e776f | ||
|
|
81c880efb1 | ||
|
|
3b3c955e2c | ||
|
|
d786509a06 | ||
|
|
7e682810fb | ||
|
|
446fc7dc35 | ||
|
|
76ccc2f8b1 | ||
|
|
6f99e6b840 | ||
|
|
08d6b11add | ||
|
|
dccf161d54 | ||
|
|
c648a8ccbc | ||
|
|
83dc9b688b | ||
|
|
2d53623ab2 | ||
|
|
d0d14b9006 | ||
|
|
db85958008 | ||
|
|
58866b0595 | ||
|
|
f6f2d5e4de | ||
|
|
118aa4bda1 | ||
|
|
ba9b4617f7 | ||
|
|
a3646e83c8 | ||
|
|
b51b8e9c63 | ||
|
|
3638372602 | ||
|
|
647a0d8a2f | ||
|
|
0bcaf9cb41 | ||
|
|
52c6fb5d06 | ||
|
|
a1a192946c | ||
|
|
713b199f21 | ||
|
|
2364e8dd26 | ||
|
|
60f98c84d5 | ||
|
|
dfea87ed0e | ||
|
|
3010657620 | ||
|
|
35477a5edb | ||
|
|
793bb97909 | ||
|
|
ca4e9c2ea5 | ||
|
|
fa68e989c4 | ||
|
|
47291206ae | ||
|
|
77578174b7 | ||
|
|
f0f7b9e6e4 | ||
|
|
dbb87a6a79 | ||
|
|
8d8c1c505c | ||
|
|
970db2b6d1 | ||
|
|
330e0818fa | ||
|
|
7f4f48aa32 | ||
|
|
fb4300fe4d | ||
|
|
4e89b94916 | ||
|
|
0f76d42b8c | ||
|
|
6cc3060dc2 | ||
|
|
83d629c397 | ||
|
|
b7fe9ba794 | ||
|
|
c0594c5449 | ||
|
|
0eb562adf9 | ||
|
|
181bd88902 | ||
|
|
3d6cfd2457 | ||
|
|
bcb5191ecc | ||
|
|
5581283c52 | ||
|
|
c3cb530aa1 | ||
|
|
5cc0665a9d | ||
|
|
6c0dde8e79 | ||
|
|
a009e06fdf | ||
|
|
4282169fc9 | ||
|
|
7c76e72a7f | ||
|
|
e22e514d29 | ||
|
|
edfbc6ac95 | ||
|
|
a23a1fa928 | ||
|
|
8b2bb48dbc | ||
|
|
930183b0aa | ||
|
|
bfbf1c5e71 | ||
|
|
67684ee0e6 | ||
|
|
336ccd2ec4 | ||
|
|
21f4a79dd5 | ||
|
|
3e94b2854c | ||
|
|
9539060404 | ||
|
|
9f73524dd9 | ||
|
|
321360e180 | ||
|
|
8ac3d0a2a6 | ||
|
|
b80118c6d0 | ||
|
|
d795812fd4 | ||
|
|
aa945e491c | ||
|
|
1e566808cb | ||
|
|
6eaa1e9edd | ||
|
|
a2d53ce217 | ||
|
|
39707005bc | ||
|
|
dc8d24322f | ||
|
|
912ee35721 | ||
|
|
9321eea8f8 | ||
|
|
347503d96f | ||
|
|
fca3aade63 | ||
|
|
a2cd1fa8ef | ||
|
|
d9db3a6edf | ||
|
|
1d749eba48 | ||
|
|
22917b7e0a | ||
|
|
e4ca35cae8 | ||
|
|
0c74ee2602 | ||
|
|
40e533d9d8 | ||
|
|
01f4bb9494 | ||
|
|
e386c8e87c | ||
|
|
bf3b1b03e8 | ||
|
|
a69c1534f6 | ||
|
|
41671d7acf | ||
|
|
a909fc1fa8 | ||
|
|
919a49b22e | ||
|
|
9ae599cf05 | ||
|
|
5aad37c613 | ||
|
|
69e580e37d | ||
|
|
c735195d33 | ||
|
|
857f0d2dcc | ||
|
|
9b6e1f4c64 | ||
|
|
8a41b35317 | ||
|
|
f08cb0f0b5 | ||
|
|
bbf1e373ad | ||
|
|
069403ab6c | ||
|
|
73ac435b92 | ||
|
|
4b439dc7bb | ||
|
|
4c7879f423 | ||
|
|
fc7540ed1e | ||
|
|
08fd9b98c8 | ||
|
|
1af8cee719 | ||
|
|
ca6c9c454d | ||
|
|
8adcc14422 | ||
|
|
65777750ab | ||
|
|
96b437c0b4 | ||
|
|
d3ae5c38d9 | ||
|
|
c6452b2fea | ||
|
|
caf1900484 | ||
|
|
2e79d34f75 | ||
|
|
28566c4272 | ||
|
|
28fd2d18fa | ||
|
|
df7ce97d40 | ||
|
|
5b2504ba3f | ||
|
|
cbf4d17c7f | ||
|
|
642ba05dcf | ||
|
|
b2b5e87aed | ||
|
|
3562f3192f | ||
|
|
e58d8b35b8 | ||
|
|
2761a3778a | ||
|
|
1eaa8d9b67 | ||
|
|
169b292301 | ||
|
|
249cfdb78d | ||
|
|
3f34e17bd4 | ||
|
|
196b48cbd3 | ||
|
|
01bfa59c6a | ||
|
|
a921ef1ec7 | ||
|
|
d30f2b9d16 | ||
|
|
8f887e5242 | ||
|
|
f742a65af8 | ||
|
|
868ba6d232 | ||
|
|
47384bec0e | ||
|
|
2144de87d4 | ||
|
|
7a3c9c5b49 | ||
|
|
df35468771 | ||
|
|
5ac9d3fdff | ||
|
|
de73ed1986 | ||
|
|
da2a92b74a | ||
|
|
9aba2db8ce | ||
|
|
5141be475b | ||
|
|
9f8e7616d1 | ||
|
|
f7c7cdef8c | ||
|
|
98e529304f | ||
|
|
c04290493e | ||
|
|
12695faba2 | ||
|
|
c1a064e40a | ||
|
|
7798594071 | ||
|
|
34281c2850 | ||
|
|
17365a6e2c | ||
|
|
9b150e3263 | ||
|
|
a1648c70c6 | ||
|
|
1a0aa37992 | ||
|
|
ad4772ba01 | ||
|
|
78b6e16eb3 | ||
|
|
682c61d025 | ||
|
|
b026479f78 | ||
|
|
1d6fbba6d2 | ||
|
|
142a47346d | ||
|
|
406d1023e8 | ||
|
|
895db0526b | ||
|
|
d2ecf48af9 | ||
|
|
edca8bec50 | ||
|
|
b99571c28f | ||
|
|
70d9d9273b | ||
|
|
bcd0b0cd46 | ||
|
|
49238a1283 | ||
|
|
7a11129a5e | ||
|
|
395fc13142 | ||
|
|
3b594d239c | ||
|
|
e7701ee0f9 | ||
|
|
80e8f5db8a | ||
|
|
608eea8e7d | ||
|
|
7dae92b042 | ||
|
|
62bc67cbe1 | ||
|
|
fbcbf0afe5 | ||
|
|
6b06873568 | ||
|
|
2007a8d05c | ||
|
|
1007772ef3 | ||
|
|
23785ea8b6 | ||
|
|
10a6b43a97 | ||
|
|
72e1d8177c | ||
|
|
6ab9bfa0fa | ||
|
|
faeb73d8b7 | ||
|
|
beff3d4832 | ||
|
|
29a4fdb47a | ||
|
|
931411b6e4 | ||
|
|
8157172a9f | ||
|
|
5ac73d9f90 | ||
|
|
27085243c4 | ||
|
|
16b2fd93d8 | ||
|
|
edce6ed9e8 | ||
|
|
e9b6db6994 | ||
|
|
f5a1671e86 | ||
|
|
560d8ec5c0 | ||
|
|
4e7a3fe515 | ||
|
|
1ac28b5203 | ||
|
|
9b69d79a78 | ||
|
|
9e78a4ef99 | ||
|
|
8d2ef41d54 | ||
|
|
c507e960fd | ||
|
|
490f700e37 | ||
|
|
355f379f21 | ||
|
|
e69f8a2e7a | ||
|
|
450d5b6144 | ||
|
|
41fdc485b4 | ||
|
|
4d25aee6dc | ||
|
|
46574f8137 | ||
|
|
71cbd1221b | ||
|
|
7386a9b614 | ||
|
|
d5cfc39744 | ||
|
|
8a1770ffda | ||
|
|
01e51ecb73 | ||
|
|
43a62813e6 | ||
|
|
1b25438fca | ||
|
|
5858de4860 | ||
|
|
8128a3374b | ||
|
|
32cc860eda | ||
|
|
03d6687c64 | ||
|
|
51924d16db | ||
|
|
cd167313b7 | ||
|
|
365142c860 | ||
|
|
990f6f148a | ||
|
|
1eb6f33207 | ||
|
|
2d23a5b37d | ||
|
|
65f400cf46 | ||
|
|
84dfdfc11c | ||
|
|
ecb1cb8a0e | ||
|
|
b9a8da1231 | ||
|
|
27c9bee138 | ||
|
|
9902718082 | ||
|
|
270ab5e843 | ||
|
|
62961889d5 | ||
|
|
0217c4b4b2 | ||
|
|
1c950a8a57 | ||
|
|
f5563ccedb | ||
|
|
0672477817 | ||
|
|
c28a1f740e | ||
|
|
e0f3b65251 | ||
|
|
001acda866 | ||
|
|
eb9ceb4dc8 | ||
|
|
d622f9075a | ||
|
|
76699ad323 | ||
|
|
a5fc5c1e40 | ||
|
|
cb9302b2fe | ||
|
|
254ddf8dfb | ||
|
|
4995cc8876 | ||
|
|
1f120a7faf | ||
|
|
4411910930 | ||
|
|
931dc04ad2 | ||
|
|
e6b390addc | ||
|
|
a4027c0ad8 | ||
|
|
3451fbf42a | ||
|
|
65e3c24de6 | ||
|
|
90e74c1c3a | ||
|
|
4610729a82 | ||
|
|
ab8c2c8b10 | ||
|
|
bf0a5e13ae | ||
|
|
e4f8f7852f | ||
|
|
348365b8fd | ||
|
|
5c2f62defa | ||
|
|
bd5a98fe13 | ||
|
|
8a0da67705 | ||
|
|
479cbbf671 | ||
|
|
6708ca00b5 | ||
|
|
53e08bbf55 | ||
|
|
d389050a66 | ||
|
|
5d4f628b8b | ||
|
|
2ecbd26477 | ||
|
|
427e0d895f | ||
|
|
dfb86d40a5 | ||
|
|
b08c7cc9e6 | ||
|
|
2bbe9464b8 | ||
|
|
ef5937d5ee | ||
|
|
289846a53a | ||
|
|
6bc361bd0c | ||
|
|
47274b0085 | ||
|
|
d3ebd83c32 | ||
|
|
b076a57811 | ||
|
|
e9285ad46d | ||
|
|
5d91a1159e | ||
|
|
ee65c22a40 | ||
|
|
1cb5c9a711 | ||
|
|
f22a3e081d | ||
|
|
fff6a529e5 | ||
|
|
2260957b00 | ||
|
|
42c845bb72 | ||
|
|
8b7ae19508 | ||
|
|
c13274d0a7 | ||
|
|
2a424ca3a9 | ||
|
|
8effb2cb76 | ||
|
|
a03ce0e82c | ||
|
|
c0913a1b05 | ||
|
|
f504581468 | ||
|
|
f4528151ac | ||
|
|
2475ea9ec9 | ||
|
|
cc380a99f1 | ||
|
|
7c34720cf7 | ||
|
|
6debbe6853 | ||
|
|
d9cf784498 | ||
|
|
12b8ec740b | ||
|
|
ff6d492857 | ||
|
|
4ea29526b1 | ||
|
|
aae3772a3a | ||
|
|
3639ced11f | ||
|
|
a5dfcd4eb6 | ||
|
|
204191973e | ||
|
|
39b55f9045 | ||
|
|
f02bb9d4ba | ||
|
|
8b897c8a55 | ||
|
|
f25bc35ee3 | ||
|
|
81a41fb941 | ||
|
|
c714ac2175 | ||
|
|
f131bcb0d9 | ||
|
|
07627b9320 | ||
|
|
f32dab48a4 | ||
|
|
61abb8cf0d | ||
|
|
bbbee3421d | ||
|
|
972a79309b | ||
|
|
d318606102 | ||
|
|
8b057ea199 | ||
|
|
edaa9f885a | ||
|
|
616760cbbb | ||
|
|
6a120f5839 | ||
|
|
3fa7546ba6 | ||
|
|
b7fd4d094c | ||
|
|
fb92e06d62 | ||
|
|
744573817a | ||
|
|
b84dc7b721 | ||
|
|
e88191c54a | ||
|
|
a32bc8c378 | ||
|
|
cb5be7cec7 | ||
|
|
209eb9c397 | ||
|
|
58b6b1cd0c | ||
|
|
b8c27ec2a9 | ||
|
|
a17c42e07e | ||
|
|
cb9a2d7b8b | ||
|
|
a7b215fa19 | ||
|
|
367170d33b | ||
|
|
695cb1496d | ||
|
|
741ca2646e | ||
|
|
2ac4b923fe | ||
|
|
cac15b1436 | ||
|
|
e77281cfb6 | ||
|
|
81d0a71060 | ||
|
|
66cb1c28f7 | ||
|
|
051e3eb922 | ||
|
|
bd93dd543e | ||
|
|
1382ef1724 | ||
|
|
6004850792 | ||
|
|
79d43f76b3 | ||
|
|
d387287a04 | ||
|
|
413e209b0b | ||
|
|
1124069e52 | ||
|
|
3d0009f23e | ||
|
|
4df0cf1049 | ||
|
|
eb91e10661 | ||
|
|
88dbfba282 | ||
|
|
f6358f10f2 | ||
|
|
51b5c81aac | ||
|
|
20ff40896f | ||
|
|
df78d375fe | ||
|
|
206194353c | ||
|
|
dcd16704bd | ||
|
|
2b6b2a5e95 | ||
|
|
c767a3b3ad | ||
|
|
06a4eac1c9 | ||
|
|
ffba3ef18a | ||
|
|
1a6d6e1290 | ||
|
|
e75dedcffa | ||
|
|
a6e50452f0 | ||
|
|
008dd2f1d7 | ||
|
|
82c3f32a5e | ||
|
|
5c0a03e242 | ||
|
|
e25ce911ed | ||
|
|
b032002c33 | ||
|
|
667bd138c8 | ||
|
|
f0f017713e | ||
|
|
331ae6ff44 | ||
|
|
ee05f34963 | ||
|
|
2f6aa3dc0f | ||
|
|
c9c5ae0058 | ||
|
|
bff5a13c25 | ||
|
|
68781783c4 | ||
|
|
04ba6bdd7b | ||
|
|
f557420c7a | ||
|
|
8d2d633f6d | ||
|
|
39ed24d133 | ||
|
|
94ec700c78 | ||
|
|
92ac19fa2e | ||
|
|
c96aeb25e5 | ||
|
|
63ebd462b9 | ||
|
|
68fe024d5a | ||
|
|
12c0207f22 | ||
|
|
402eade4c2 | ||
|
|
c3f88773d2 | ||
|
|
5c689ebf9b | ||
|
|
efa99cb9ac | ||
|
|
19a96aac00 | ||
|
|
6c84d433f6 | ||
|
|
63c4ab822c | ||
|
|
f343397a68 | ||
|
|
3993f5a715 | ||
|
|
ac66b6ac1b | ||
|
|
de5c07d74b | ||
|
|
4f8a9b4d3f | ||
|
|
1e643b3a99 | ||
|
|
eddbe33f2e | ||
|
|
6b07e07bea | ||
|
|
8ff8502fed | ||
|
|
31ee93a2a0 | ||
|
|
ce17685154 | ||
|
|
4927e6135d | ||
|
|
6725873c8c | ||
|
|
90b1a244a5 | ||
|
|
c3a7191f31 | ||
|
|
e3a2a98c2f | ||
|
|
35aaafeba6 | ||
|
|
0c9d3c9152 | ||
|
|
6a93608427 | ||
|
|
c2b84ce240 | ||
|
|
e6b876314c | ||
|
|
9cb73fe9fc | ||
|
|
f4f0ae990f | ||
|
|
50c314af7d | ||
|
|
bd18a5bb41 | ||
|
|
263faa9895 | ||
|
|
36cc45b227 | ||
|
|
66ce27593b | ||
|
|
9a62690627 | ||
|
|
26dcee20dc | ||
|
|
8c73de6f94 | ||
|
|
8a3a3d05c4 | ||
|
|
11d78d0b44 | ||
|
|
ae3fb2d41e | ||
|
|
54d66c0119 | ||
|
|
beb37bad9a | ||
|
|
cb25353e60 | ||
|
|
7bd24f52c7 | ||
|
|
bfd646f409 | ||
|
|
ec5ade7b38 | ||
|
|
4bb6a64153 | ||
|
|
e7a29f6626 | ||
|
|
7d168d7f24 | ||
|
|
8b0e10442f | ||
|
|
10ed279650 | ||
|
|
18300fb1a2 | ||
|
|
82543ee67e | ||
|
|
58c8a23279 | ||
|
|
6b7db83a40 | ||
|
|
3d5a7e9bc7 | ||
|
|
d1e8f9db6b | ||
|
|
532e6bee5b | ||
|
|
a7af611dfb | ||
|
|
de1296f750 | ||
|
|
43e1c20046 | ||
|
|
b285205376 | ||
|
|
db4ec5eb07 | ||
|
|
51e6b6c908 | ||
|
|
e57ab4cdb0 | ||
|
|
c4d4a7f2c3 | ||
|
|
66045abf16 | ||
|
|
019af5d238 | ||
|
|
d71d10a97f | ||
|
|
f374466d0a | ||
|
|
97d9c57951 | ||
|
|
b651c1f9e9 | ||
|
|
d448b59dcc | ||
|
|
06ccc2d9c6 | ||
|
|
bf2ee271fd | ||
|
|
b9486e8853 | ||
|
|
7911f9bd20 | ||
|
|
6753039b92 | ||
|
|
802eff0796 | ||
|
|
ff8141705d | ||
|
|
b28f8f50e1 | ||
|
|
49a1f9527c | ||
|
|
240f5e6419 | ||
|
|
2f26cbe676 | ||
|
|
2a304bd413 | ||
|
|
189b0ea122 | ||
|
|
70b24f9b86 | ||
|
|
6c11701032 | ||
|
|
d4a28c0e7b | ||
|
|
0e50de91b4 | ||
|
|
ac78b96c2b | ||
|
|
aca091e3c3 | ||
|
|
572e7f5be8 | ||
|
|
ef4ab9570a | ||
|
|
6207299219 | ||
|
|
8eb8c666b6 | ||
|
|
8f40e8ae04 | ||
|
|
b93131b990 | ||
|
|
6bce621da4 | ||
|
|
9e9bc5193f | ||
|
|
178f937727 | ||
|
|
d94abfaef9 | ||
|
|
c03da7eb74 | ||
|
|
c39313d973 | ||
|
|
a0c126f39e | ||
|
|
354d184664 | ||
|
|
0e49d103b1 | ||
|
|
fbbae218c0 | ||
|
|
12387f0efb | ||
|
|
41be0bb069 | ||
|
|
6edcb95aab | ||
|
|
f022419ceb | ||
|
|
9232042b02 | ||
|
|
1c642a3cb2 | ||
|
|
b5e86473a4 | ||
|
|
8417f5cb7a | ||
|
|
f434be45b6 | ||
|
|
6d493aeb9c | ||
|
|
b4052039e3 | ||
|
|
727689a3bd | ||
|
|
b86f9792f3 | ||
|
|
83f4a5ea73 | ||
|
|
80ddea2c8c | ||
|
|
6618f71297 | ||
|
|
5c1c039188 | ||
|
|
af1afb5348 | ||
|
|
5bbd527837 | ||
|
|
2eb71fe1c9 | ||
|
|
e1157ad01a | ||
|
|
e5df3b9cb3 | ||
|
|
a52f7dc540 | ||
|
|
eff69eba7d | ||
|
|
f8a41b39ec | ||
|
|
cd31806100 | ||
|
|
e5d588c4eb | ||
|
|
3b0483752c | ||
|
|
5ea5ca0ef7 | ||
|
|
08bd2c4724 | ||
|
|
1c1a4d92f4 | ||
|
|
37e5dda38f | ||
|
|
be6232ee82 | ||
|
|
651fa42e4a | ||
|
|
c69d261e56 | ||
|
|
54446949a5 | ||
|
|
161d4ed314 | ||
|
|
a88c08bf3f | ||
|
|
eae4a0be35 | ||
|
|
0e28b40c25 | ||
|
|
f8adbcc252 | ||
|
|
6f9d557a64 | ||
|
|
ac939de42f | ||
|
|
82756e1073 | ||
|
|
1b14148b0f | ||
|
|
0cfb5eff0e | ||
|
|
ce3bab8a33 | ||
|
|
e56b8545c7 | ||
|
|
ac7fc21db5 | ||
|
|
48bc2174a4 | ||
|
|
d11d58889b | ||
|
|
8cf01c318a | ||
|
|
e18007e902 | ||
|
|
1f13b8d43d | ||
|
|
c6b82043d0 | ||
|
|
671937ff7f | ||
|
|
27478966bc | ||
|
|
442ea94cde | ||
|
|
13bd24ef83 | ||
|
|
32240b6004 | ||
|
|
abd0880c66 | ||
|
|
bb6c5f8e44 | ||
|
|
66d0141fae | ||
|
|
57cf7a582b | ||
|
|
706827fc0c | ||
|
|
54009dcb06 | ||
|
|
1049deb299 | ||
|
|
19b928782a | ||
|
|
4aafafb5bd | ||
|
|
65ff5a8c51 | ||
|
|
d2449c181a | ||
|
|
9527a33a58 | ||
|
|
419a513d8f | ||
|
|
a1e7ecbecc | ||
|
|
f996c90653 | ||
|
|
be2a7f5a51 | ||
|
|
a6ca193ae0 | ||
|
|
ff7f81c307 | ||
|
|
cad42948cd | ||
|
|
5ad88099e1 | ||
|
|
c849c2b867 | ||
|
|
1571263575 | ||
|
|
a335d60fcb | ||
|
|
c78c182e32 | ||
|
|
8ff5d05078 | ||
|
|
80a1c13a07 | ||
|
|
0b6ed46a0a | ||
|
|
cdaba3483a | ||
|
|
3681dba5fb | ||
|
|
ddbe5af21c | ||
|
|
da4263131e | ||
|
|
f3b771cb7d | ||
|
|
39c764e283 | ||
|
|
9d4e02c2d4 | ||
|
|
34ab157046 | ||
|
|
e76403f54f | ||
|
|
3f7b1ce204 | ||
|
|
0eff1225b8 | ||
|
|
4e2d024b2a | ||
|
|
1db289c35e | ||
|
|
c8a4a41eaf | ||
|
|
6221f87b8c | ||
|
|
ab3b682c47 | ||
|
|
38d6ab9895 | ||
|
|
4d6da7454c | ||
|
|
19ac30eed0 | ||
|
|
84a7b79e3c | ||
|
|
e2fc8015b8 | ||
|
|
5aac30f44a | ||
|
|
d9340349fa | ||
|
|
82019b5f40 | ||
|
|
06e0a82927 | ||
|
|
53409a6c70 | ||
|
|
59b68d80ca | ||
|
|
4f38f5a389 | ||
|
|
7a0bb07fc1 | ||
|
|
b1d3e712be | ||
|
|
bc0cd4a783 | ||
|
|
d4dc65c45f | ||
|
|
dd40a1de6b | ||
|
|
ea65a3423e | ||
|
|
04811da9cc | ||
|
|
46a4243093 | ||
|
|
03bee3e11e | ||
|
|
20491e1136 | ||
|
|
4ea01ea135 | ||
|
|
8440f72e9e | ||
|
|
65291259ee | ||
|
|
d42ee05740 | ||
|
|
c4bf644240 | ||
|
|
fe95ad2e11 | ||
|
|
644f80df57 | ||
|
|
46e5c3233a | ||
|
|
1da27edde6 | ||
|
|
75da8833dc | ||
|
|
c40a132811 | ||
|
|
288267ae11 | ||
|
|
8f1a493327 | ||
|
|
f97adc325f | ||
|
|
8aa72e9397 | ||
|
|
3ab7886eab | ||
|
|
551968a38c | ||
|
|
e954102f81 | ||
|
|
5a1d5c0a2d | ||
|
|
42b553266a | ||
|
|
d98d2915ca | ||
|
|
634dced5aa | ||
|
|
5ee94199bb | ||
|
|
23422220ce | ||
|
|
c141462432 | ||
|
|
41abf876a2 | ||
|
|
28793abc7a | ||
|
|
1ce213004a | ||
|
|
e223fa2002 | ||
|
|
6fc542ff7d | ||
|
|
a1dd4ab0e9 | ||
|
|
1232f1be28 | ||
|
|
738c7b968a | ||
|
|
d6e7d1ffee | ||
|
|
6fcdee14e4 | ||
|
|
8c139ad044 | ||
|
|
122baba999 | ||
|
|
27b12f5263 | ||
|
|
a801424b35 | ||
|
|
717a3eaede | ||
|
|
0061f35b46 | ||
|
|
ff20646a63 | ||
|
|
a5f4375ff2 | ||
|
|
99b2cbb606 | ||
|
|
15e8eee373 | ||
|
|
4cd2bc266a | ||
|
|
92472c815a | ||
|
|
a246257eff | ||
|
|
b6bab552be | ||
|
|
1afce00069 | ||
|
|
9ddfdf3638 | ||
|
|
06230ffa40 | ||
|
|
cac7ac2a8a | ||
|
|
fbbe4f5579 | ||
|
|
7d8373643f | ||
|
|
97bf88421e | ||
|
|
c067ab7cc9 | ||
|
|
b21e4f5296 | ||
|
|
ea0add13ad | ||
|
|
bfc3157186 | ||
|
|
a5aeb43305 | ||
|
|
2b77a15845 | ||
|
|
0d1b751600 | ||
|
|
754e2a7aa7 | ||
|
|
731f375332 | ||
|
|
faa8013208 | ||
|
|
90afe81856 | ||
|
|
c13e379908 | ||
|
|
689d3bb9b5 | ||
|
|
9ae8fcf3bc | ||
|
|
74f3a0a5ed | ||
|
|
d51e20bffe | ||
|
|
c5d6214ab2 | ||
|
|
898921ea4f | ||
|
|
8c6194da35 | ||
|
|
8b2a3ccb42 | ||
|
|
820c869e2c | ||
|
|
9a21a3e2a4 | ||
|
|
03e47cee56 | ||
|
|
1a28f15f75 | ||
|
|
72d5c74631 | ||
|
|
322ebdf8a6 | ||
|
|
3d3296d1b7 | ||
|
|
e3715c3f81 | ||
|
|
db9e019519 | ||
|
|
3a2ea56c92 | ||
|
|
df6b552ac1 | ||
|
|
9bb4d7ed93 | ||
|
|
562ef1b147 | ||
|
|
6d31a2f656 | ||
|
|
62c16d9f08 | ||
|
|
db17973ea6 | ||
|
|
8155376c45 | ||
|
|
ae26c7bc7a | ||
|
|
fa4e4cb2bb | ||
|
|
7842b7e0fd | ||
|
|
52d6245b4a | ||
|
|
4c03d4f0b4 | ||
|
|
de8aaf35a3 | ||
|
|
dda5d8206f | ||
|
|
f8ca2a50b7 | ||
|
|
329f4072be | ||
|
|
a88b392a43 | ||
|
|
28ec418c76 | ||
|
|
ce56b87b16 | ||
|
|
6d3155dff9 | ||
|
|
9fee1fda1e | ||
|
|
1e1c5fa139 | ||
|
|
70fb1090ca | ||
|
|
6f13aa4724 | ||
|
|
584f923c4c | ||
|
|
3d1c39da30 | ||
|
|
a3148c9825 | ||
|
|
40e33a43a9 | ||
|
|
a3fc74c44e | ||
|
|
3b28b5fa1c | ||
|
|
6070dc9db9 | ||
|
|
7608f02c21 | ||
|
|
cec5ec0a78 | ||
|
|
c72c683915 | ||
|
|
3384c773da | ||
|
|
8dfc16880e | ||
|
|
5d65861c49 | ||
|
|
179b1419ab | ||
|
|
4bebc8486c | ||
|
|
fd0f6e4113 | ||
|
|
8e4c40ba2a | ||
|
|
abf1a92977 | ||
|
|
e9ff6ece35 | ||
|
|
ba02ce9261 | ||
|
|
0ece4427b6 | ||
|
|
9afc192154 | ||
|
|
27691d8563 | ||
|
|
0ebc6dd2d8 | ||
|
|
5a2f57172f | ||
|
|
47d30c81b6 | ||
|
|
b80f78d12e | ||
|
|
835ffaafd4 | ||
|
|
66f0d580b9 | ||
|
|
524998811c | ||
|
|
1b3a8d0cf5 | ||
|
|
0134d343b2 | ||
|
|
202ddd8157 | ||
|
|
76159b2cc4 | ||
|
|
620e95cebf | ||
|
|
5f5357d28a | ||
|
|
faee08bb90 | ||
|
|
178f98c82f | ||
|
|
d0dc16aa6f | ||
|
|
77e56e8da9 | ||
|
|
045d7bb5ae | ||
|
|
135f043b60 | ||
|
|
682884355f | ||
|
|
702b473453 | ||
|
|
3f803a3f48 | ||
|
|
742be996fe | ||
|
|
228d929a5c | ||
|
|
703fddeb65 | ||
|
|
be89397e48 | ||
|
|
2a71716ba4 | ||
|
|
b8b5561850 | ||
|
|
88694f4256 | ||
|
|
4d7126d3c3 | ||
|
|
87ab6c1de2 | ||
|
|
e649c9128e | ||
|
|
9b774734c3 | ||
|
|
841164bafb | ||
|
|
bc61ec0495 | ||
|
|
c37bc213e5 | ||
|
|
080639d2d9 | ||
|
|
ab2d0771ab | ||
|
|
204d502c24 | ||
|
|
985b60a565 | ||
|
|
8202c4abe3 | ||
|
|
4de73cebe0 | ||
|
|
9bb6e833fb | ||
|
|
d227da0c10 | ||
|
|
962330d491 | ||
|
|
c5d9a98549 | ||
|
|
9b83c78346 | ||
|
|
8a7d0d4242 | ||
|
|
45440adc01 | ||
|
|
8feaf9ed3e | ||
|
|
636020d870 | ||
|
|
3150aa2e7c | ||
|
|
ce22449f41 | ||
|
|
1c938b5d29 | ||
|
|
0493104beb | ||
|
|
9786d9f927 | ||
|
|
73e5254e2b | ||
|
|
7c934d29ac | ||
|
|
bccbf3e55e | ||
|
|
8196ee531a | ||
|
|
39729cb3da | ||
|
|
cdc1f6014e | ||
|
|
aadfdac4c5 | ||
|
|
2e358116c2 | ||
|
|
71d88523b2 | ||
|
|
65e702aed7 | ||
|
|
99bb45d96a | ||
|
|
4f41be641a | ||
|
|
5ab030242b | ||
|
|
736d9bbd98 | ||
|
|
6295ebdb09 | ||
|
|
380a27b83d | ||
|
|
691af037a4 | ||
|
|
70cb78a67d | ||
|
|
4bd5fe0178 | ||
|
|
babd80edd9 | ||
|
|
74e4efba52 | ||
|
|
2d7e88a8c2 | ||
|
|
d82c8d466a | ||
|
|
9edbbf399f | ||
|
|
345316b9b0 | ||
|
|
0bb6636613 | ||
|
|
1929b75e34 | ||
|
|
245827825e | ||
|
|
fc9f81c5c9 | ||
|
|
987dce2643 | ||
|
|
4f2abad39d | ||
|
|
354000082b | ||
|
|
b8e87604c2 | ||
|
|
49f7ad1281 | ||
|
|
6731cdcc9d | ||
|
|
5306510ec1 | ||
|
|
e8c0655186 | ||
|
|
18d0bf1c7b | ||
|
|
0b9755405e | ||
|
|
8b7a183e63 | ||
|
|
7a094478f3 | ||
|
|
bd063c71f8 | ||
|
|
693ef6c4d3 | ||
|
|
60a02cd2a5 | ||
|
|
e25c7dac4a | ||
|
|
d918cade67 | ||
|
|
982400b74d | ||
|
|
c00a384591 | ||
|
|
b0e1630fc5 | ||
|
|
50ffd7b4a9 | ||
|
|
e4e75a5f73 | ||
|
|
7da4381d38 | ||
|
|
ef8076b626 | ||
|
|
ab5f5c1ce9 | ||
|
|
6d442b12a0 | ||
|
|
0019f83e33 | ||
|
|
f3011b1790 | ||
|
|
ea79f84b71 | ||
|
|
040d6929dc | ||
|
|
2fe9c38fff | ||
|
|
256b54b6b5 | ||
|
|
062ad5d92f | ||
|
|
5806ef248f | ||
|
|
257fcf1b3d | ||
|
|
284db016a0 | ||
|
|
5e2898f0ae | ||
|
|
207408ae20 | ||
|
|
2843ca9b49 | ||
|
|
efc09d8a3f | ||
|
|
e6714bf45e | ||
|
|
8049e2c322 | ||
|
|
fdb0db2d41 | ||
|
|
7465031d44 | ||
|
|
15eb3a421d | ||
|
|
ec52a07fe9 | ||
|
|
b7a475cc0a | ||
|
|
aed022d099 | ||
|
|
86b37e7df0 | ||
|
|
09e9b46efd | ||
|
|
90b685c15d | ||
|
|
daedffc0da | ||
|
|
c489f88f77 | ||
|
|
33779bb6cf | ||
|
|
660806fb7c | ||
|
|
40a52ed00b | ||
|
|
c216498198 | ||
|
|
547c887c0b | ||
|
|
dfe23da44d | ||
|
|
a5e8b88e32 | ||
|
|
539c94f36f | ||
|
|
fce5ae0498 | ||
|
|
8fbf96098e | ||
|
|
c8b43fa8ab | ||
|
|
9f305347a8 | ||
|
|
94d826d2a4 | ||
|
|
118cf70e40 | ||
|
|
895d8d594b | ||
|
|
60ce467859 | ||
|
|
6efa58006e | ||
|
|
f253019daf | ||
|
|
8600aceb64 | ||
|
|
a358af534a | ||
|
|
d080676997 | ||
|
|
b3412fcfb4 | ||
|
|
b5e3db279d | ||
|
|
a29ca46ca3 | ||
|
|
2fe54a28b1 | ||
|
|
6f9bc11078 | ||
|
|
cd5186cd0d | ||
|
|
34cdc299b7 | ||
|
|
274a3f78f2 | ||
|
|
0e31ad96a5 | ||
|
|
b191e4d83a | ||
|
|
16c554aa43 | ||
|
|
80550cd42e | ||
|
|
3ac28aa279 | ||
|
|
f16ea99cbc | ||
|
|
d4353bd237 | ||
|
|
8efa82abc2 | ||
|
|
a5f42f77ad | ||
|
|
384314d9ab | ||
|
|
56d63a46bf | ||
|
|
da00ccff58 | ||
|
|
313d5ab762 | ||
|
|
a569029c40 | ||
|
|
04748cf03f | ||
|
|
91871fb96d | ||
|
|
7292a12ff4 | ||
|
|
9b04e8ff38 | ||
|
|
b85a2ea6d2 | ||
|
|
da6c134540 | ||
|
|
e09da1a600 | ||
|
|
5aa1f9863d | ||
|
|
1eeb3637e8 | ||
|
|
79e229467e | ||
|
|
d483ea7f47 | ||
|
|
4e4b31a0d5 | ||
|
|
eb38c1ec15 | ||
|
|
b33406da4d | ||
|
|
10f4da71b8 | ||
|
|
6d27a8e489 | ||
|
|
c63477c34e | ||
|
|
761d1877fa | ||
|
|
cf4dbf6ed9 | ||
|
|
53e2df8aa2 | ||
|
|
07278de968 | ||
|
|
6710c47db0 | ||
|
|
a5cc820ea6 | ||
|
|
5eb95980bf | ||
|
|
57cc0e920a | ||
|
|
902b707b5e | ||
|
|
98600e9a19 | ||
|
|
fbf8ac4883 | ||
|
|
4c7a25407a | ||
|
|
af9359850d | ||
|
|
abe5638af5 | ||
|
|
c45e8a11d7 | ||
|
|
62ef84ea07 | ||
|
|
17b5098602 | ||
|
|
272a7afe9e | ||
|
|
9c7fa1a6a8 | ||
|
|
38a050dd13 | ||
|
|
377396d77c | ||
|
|
80fc8eddb9 | ||
|
|
d9c23ece15 | ||
|
|
84a913451f | ||
|
|
59794efc95 | ||
|
|
13977a07e5 | ||
|
|
b1eb8bd8ec | ||
|
|
69043cfd1c | ||
|
|
f34c713e7e | ||
|
|
e180bf21a7 | ||
|
|
fdaaa4be30 | ||
|
|
4db445dd3e | ||
|
|
530d4c0b08 | ||
|
|
100ce6ded9 | ||
|
|
03deb78aab | ||
|
|
bb220366be | ||
|
|
cd4e4a0892 | ||
|
|
aa42c302aa | ||
|
|
5128557e4e | ||
|
|
a181f5d1db | ||
|
|
5e3bc6c507 | ||
|
|
b11a9557d7 | ||
|
|
593e172832 | ||
|
|
6b62a7fde3 | ||
|
|
9a80028ec7 | ||
|
|
1252e22743 | ||
|
|
a3a25d9bef | ||
|
|
08fa3a5768 | ||
|
|
52cd521902 | ||
|
|
6dd5f1991f | ||
|
|
d3d29e03be | ||
|
|
2ddc88dd87 | ||
|
|
f768aa360f | ||
|
|
5ba12d009b | ||
|
|
6259891312 | ||
|
|
fad59e4a0f | ||
|
|
81852fe8c6 | ||
|
|
b33e262de6 | ||
|
|
7963919b72 | ||
|
|
e7f87478b4 | ||
|
|
c6b8d95747 | ||
|
|
8468c5ac52 | ||
|
|
27d7ae4b78 | ||
|
|
fffd2afe5e | ||
|
|
15ba670d5a | ||
|
|
7e7279a642 | ||
|
|
1b765e2408 | ||
|
|
db0ae9253e | ||
|
|
5573218794 | ||
|
|
6e38f16c83 | ||
|
|
4f0d2370ac | ||
|
|
f49f677107 | ||
|
|
cbb11ea723 | ||
|
|
566d011e2e | ||
|
|
1d2a62d06c | ||
|
|
743163fce7 | ||
|
|
afe5333c88 | ||
|
|
3bf15b5a35 | ||
|
|
1c9cfbc4bb | ||
|
|
65093b0388 | ||
|
|
e2320ea77c | ||
|
|
59f414f707 | ||
|
|
2a2c12cf77 | ||
|
|
5fd34dc967 | ||
|
|
65df6765da | ||
|
|
ff9729d27a | ||
|
|
9876741c50 | ||
|
|
a1f69f0732 | ||
|
|
8174b883a0 | ||
|
|
337065271e | ||
|
|
01dd0e7fc4 | ||
|
|
f3ee08ab9a | ||
|
|
ba08c09607 | ||
|
|
aecfd12911 | ||
|
|
c081e48467 | ||
|
|
237b0324c1 | ||
|
|
5aaf39f40b | ||
|
|
7a4ed29945 | ||
|
|
84014ce9b5 | ||
|
|
08c8e84d72 | ||
|
|
43171879a0 | ||
|
|
2f7a30f920 | ||
|
|
27a93b318d | ||
|
|
816bb2008a | ||
|
|
8eb430dffd | ||
|
|
641b5af8eb | ||
|
|
38ef1e4ed9 | ||
|
|
123c41b4e3 | ||
|
|
7001f17c88 | ||
|
|
1f10191bc7 | ||
|
|
ed5f5d3c6b | ||
|
|
a03bb7a353 | ||
|
|
44d4eae8a1 | ||
|
|
22ffcf5660 | ||
|
|
f40ea6cc8d | ||
|
|
adfcb2b4bf | ||
|
|
8d42be3bac | ||
|
|
e2036a5203 | ||
|
|
7107f05adb | ||
|
|
529fc42b72 | ||
|
|
7808605a8c | ||
|
|
0930013cc2 | ||
|
|
51054b70d6 | ||
|
|
471fb621e5 | ||
|
|
1e0c3e7621 | ||
|
|
a75d312770 | ||
|
|
0fd2ec58be | ||
|
|
be484a8077 | ||
|
|
3d8388ad92 | ||
|
|
7a5eac3621 | ||
|
|
ce34e937d6 | ||
|
|
c8646208f0 | ||
|
|
3ed83e4f59 | ||
|
|
ebe6a12555 | ||
|
|
1727456487 | ||
|
|
ef895777ca | ||
|
|
33fa10e9c1 | ||
|
|
1f9094caec | ||
|
|
9a5c58aae8 | ||
|
|
1b1e6c394d | ||
|
|
504872ac1e | ||
|
|
edc40f4a6c | ||
|
|
7b6e08f791 | ||
|
|
b184db2ee6 | ||
|
|
d10bf7fa35 | ||
|
|
ce20d21f4c | ||
|
|
48a35746a0 | ||
|
|
bf9afd4ba1 | ||
|
|
0eb2d59ecc | ||
|
|
b9bcfef817 | ||
|
|
27d01500fa | ||
|
|
d2ea3e4986 | ||
|
|
b0cecebe34 | ||
|
|
f4971589c6 | ||
|
|
506d64dc8b | ||
|
|
f56aa6cc29 | ||
|
|
a17ec8c2db | ||
|
|
a718986e2d | ||
|
|
242e0e7fdc | ||
|
|
75f7cb6df7 | ||
|
|
cf7d0792a2 | ||
|
|
29d260db9d | ||
|
|
d7dfad1fe8 | ||
|
|
82f404e627 | ||
|
|
c49b4c45c8 | ||
|
|
39f3feb210 | ||
|
|
4ed89e9515 | ||
|
|
0b6989c60c | ||
|
|
a9139c6023 | ||
|
|
2de580236e | ||
|
|
e5d4979f3e | ||
|
|
8f00de273a | ||
|
|
1a0f5a7460 | ||
|
|
b0229fb528 | ||
|
|
454f1aa00a | ||
|
|
12c25d786f | ||
|
|
7c65eca1fb | ||
|
|
6a87988d4c | ||
|
|
b65a17c867 | ||
|
|
f067fc9896 | ||
|
|
36cc53b4f8 | ||
|
|
497df9db96 | ||
|
|
26f66b0377 | ||
|
|
69c448f4e6 | ||
|
|
df08191857 | ||
|
|
bb88ff7649 | ||
|
|
7329bf8eef | ||
|
|
a58e409665 | ||
|
|
511a55e22c | ||
|
|
cfa2cfeded | ||
|
|
9d30414539 | ||
|
|
1b34de81eb | ||
|
|
827824500b | ||
|
|
35198ddbe8 | ||
|
|
546545a800 | ||
|
|
1023cc5cb2 | ||
|
|
094cf9e400 | ||
|
|
1bc834efe8 | ||
|
|
b4b039b2b0 | ||
|
|
d8e850673b | ||
|
|
35d7046d69 | ||
|
|
fad87abfea | ||
|
|
10d10ff18d | ||
|
|
161b99f252 | ||
|
|
87ff260c9f | ||
|
|
cc4dafa400 | ||
|
|
9d29904ec5 | ||
|
|
50be4f4ef8 | ||
|
|
5f9c05080f | ||
|
|
bbfcc87a68 | ||
|
|
d7e9510353 | ||
|
|
1a1057f56f | ||
|
|
93ff8cfe1b | ||
|
|
4d05e085ef | ||
|
|
d2d96d2ece | ||
|
|
78762cd479 | ||
|
|
cdaa37c5fd | ||
|
|
76140fb555 | ||
|
|
352c5ea3d3 | ||
|
|
f167d4fa3b | ||
|
|
7528cd3b58 | ||
|
|
cab8d0fb97 | ||
|
|
c7d9c174fd | ||
|
|
b7787c8cf6 | ||
|
|
4d218e3624 | ||
|
|
89fab45eb7 | ||
|
|
9bca338cc2 | ||
|
|
36c3c43f86 | ||
|
|
d9f762c485 | ||
|
|
3d206d1fb1 | ||
|
|
2a959f23d9 | ||
|
|
bb659d39db | ||
|
|
49cdd4d36d | ||
|
|
41d55cf780 | ||
|
|
5c5c948193 | ||
|
|
0e32dc982e | ||
|
|
9fffdb8cad | ||
|
|
76a1da2d0f | ||
|
|
ba012a4b0e | ||
|
|
09fd212e29 | ||
|
|
54fd4787fd | ||
|
|
023499a6c0 | ||
|
|
a8ad7f540b | ||
|
|
b7ee2c696b | ||
|
|
4528223dee | ||
|
|
cf729db0c0 | ||
|
|
a4deafa238 | ||
|
|
5e88eab617 | ||
|
|
950e51e0fe | ||
|
|
1c1d743638 | ||
|
|
ffa7e39121 | ||
|
|
84d6a24446 | ||
|
|
91c92d4b64 | ||
|
|
056229e8ee | ||
|
|
c4c5f7ab6b | ||
|
|
c7ab33f3ab | ||
|
|
3a3850ec4e | ||
|
|
84ed014909 | ||
|
|
39563682f5 | ||
|
|
53179f6e3f | ||
|
|
b1d5ba6787 | ||
|
|
361a3d0371 | ||
|
|
6b5b38535e | ||
|
|
8a4539b19a | ||
|
|
f6ee232d9e | ||
|
|
2562d28f4f | ||
|
|
f89cfa3ec8 | ||
|
|
d8b665f64f | ||
|
|
d473737ca8 | ||
|
|
52e7cc41e2 | ||
|
|
53978333fb | ||
|
|
183209baa3 | ||
|
|
0df4e1147c | ||
|
|
7c42b1f845 | ||
|
|
129e4762dc | ||
|
|
fc1c5ca6e9 | ||
|
|
8ae916ac28 | ||
|
|
0ccf9c961b | ||
|
|
b894e65618 | ||
|
|
801d31ed29 | ||
|
|
34fb9bb865 | ||
|
|
b5201e17ff | ||
|
|
204bbfc6c7 | ||
|
|
692fe0c885 | ||
|
|
3a372179d6 | ||
|
|
3de8bd49ad | ||
|
|
b9c02d5d99 | ||
|
|
185c4c09c9 | ||
|
|
ad709fd8f0 | ||
|
|
da7a1f34e1 | ||
|
|
47d2f3b6ad | ||
|
|
673759f17b | ||
|
|
0e2d6ac196 | ||
|
|
eb99915912 | ||
|
|
501df02260 | ||
|
|
d74f6dba0a | ||
|
|
b590c6b55a | ||
|
|
175f38f114 | ||
|
|
44f69b2b0b | ||
|
|
02a1b139bf | ||
|
|
326cf81a0f | ||
|
|
8952ddc9dc | ||
|
|
dfe2c6d064 | ||
|
|
fea2f30a1c | ||
|
|
5d8bd256f1 | ||
|
|
d7f41f4a56 | ||
|
|
9169ea6ebf | ||
|
|
20314c8952 | ||
|
|
f262075d2a | ||
|
|
37d9ea103b | ||
|
|
ef51f23308 | ||
|
|
11f75176b8 | ||
|
|
bd676c7c5c | ||
|
|
ccf51356a9 | ||
|
|
ece32cbe70 | ||
|
|
574e74350c | ||
|
|
4665f8c6c6 | ||
|
|
a1bdd1fc47 | ||
|
|
cdcb801b50 | ||
|
|
5f4bfed2b5 | ||
|
|
cffaa6fef8 | ||
|
|
bf2e5392f8 | ||
|
|
de6c93b849 | ||
|
|
3b6bbe4799 | ||
|
|
62250cd41f | ||
|
|
69a3963de0 | ||
|
|
acefebfcd7 | ||
|
|
b8f7d084c7 | ||
|
|
fd420b3d81 | ||
|
|
bf799869ef | ||
|
|
8fc3004349 | ||
|
|
13b22e2047 | ||
|
|
2a33ba9f93 | ||
|
|
ad09233d4c | ||
|
|
2be3a38bb0 | ||
|
|
26467221b3 | ||
|
|
dd19ad356a | ||
|
|
4fe91e9a6a | ||
|
|
63d660d152 | ||
|
|
730ff1758f | ||
|
|
174099dd1d | ||
|
|
f7f49b98ff | ||
|
|
18f0af1803 | ||
|
|
af2a4e1ce4 | ||
|
|
a4a58d9a06 | ||
|
|
861bb5f608 | ||
|
|
36c7f1244d | ||
|
|
8e4d07163d | ||
|
|
1af3cd40c0 | ||
|
|
8e0eb9d110 | ||
|
|
bf7dbe0098 | ||
|
|
f37d27f803 | ||
|
|
b98d2fbaa4 | ||
|
|
181539a424 | ||
|
|
6fe299d7c8 | ||
|
|
585eb23b3c | ||
|
|
04940fc4db | ||
|
|
75fa1b7fbe | ||
|
|
cc8e8b2dd1 | ||
|
|
0b3bbad98f | ||
|
|
9da11e05ee | ||
|
|
ecca6f8eda | ||
|
|
b5c6f32d1f | ||
|
|
f38c083b05 | ||
|
|
598a9e0918 | ||
|
|
c93c582ed5 | ||
|
|
010cfb6b35 | ||
|
|
b1d95a5fa0 | ||
|
|
0d4bf2ea2e | ||
|
|
d4eb451ba6 | ||
|
|
9435701d10 | ||
|
|
7603943c38 | ||
|
|
a959f79bcf | ||
|
|
9899d72dce | ||
|
|
4263ea6881 | ||
|
|
ab287858b3 | ||
|
|
eb6d168d50 | ||
|
|
52b8c4ae5e | ||
|
|
1c4640d69b | ||
|
|
3827459229 | ||
|
|
aaeb9f9e84 | ||
|
|
61acb3e42b | ||
|
|
4a8f299bb3 | ||
|
|
5530ca94f3 | ||
|
|
61a1b888bd | ||
|
|
a583701319 | ||
|
|
57623c710a | ||
|
|
93488528de | ||
|
|
e7a5ffdf5f | ||
|
|
dc56cb382b | ||
|
|
bb22ea3c8f | ||
|
|
b856ad0374 | ||
|
|
3aff3c51a8 | ||
|
|
3e096a25f7 | ||
|
|
adb21e4a7e | ||
|
|
3ea268aea7 | ||
|
|
567a8913e4 | ||
|
|
fd89b30192 | ||
|
|
ce3b327cff | ||
|
|
4e5ac4a921 | ||
|
|
b44c1478b8 | ||
|
|
b0890ad4bf | ||
|
|
83d632e24c | ||
|
|
1e5874aed5 | ||
|
|
e807d9a3e7 | ||
|
|
17bb74ff25 | ||
|
|
5c9e2839eb | ||
|
|
d670a761d9 | ||
|
|
b6a3f765a7 | ||
|
|
2ca2ceea5a | ||
|
|
01b4ab512c | ||
|
|
d18a6c3479 | ||
|
|
ab931cdcb4 | ||
|
|
7252b0e69a | ||
|
|
ab0ed23c83 | ||
|
|
18c002094f | ||
|
|
d5f73cb633 | ||
|
|
c738da065e | ||
|
|
a9296d67f7 | ||
|
|
78cb417cfc | ||
|
|
4bec5f6792 | ||
|
|
3da03f66e0 | ||
|
|
e379c676d0 | ||
|
|
6565494781 | ||
|
|
aafcd25403 | ||
|
|
72c4c0007f | ||
|
|
fd1ce7a750 | ||
|
|
fbbdec5e4e | ||
|
|
443b613513 | ||
|
|
2fa44fc0f4 | ||
|
|
5ed21c3ce3 | ||
|
|
f1d52fb530 | ||
|
|
d418ad8369 | ||
|
|
7b58d8e77e | ||
|
|
38971c2946 | ||
|
|
6cef7fd6b6 | ||
|
|
92ffb0b1e5 | ||
|
|
75f03f1fd1 | ||
|
|
0306817ac8 | ||
|
|
03b5dc046d | ||
|
|
bb5cb8a773 | ||
|
|
719bd257a6 | ||
|
|
e21257ab71 | ||
|
|
fc933ba9d5 | ||
|
|
9f9215cb3f | ||
|
|
8308c21d39 | ||
|
|
1f6ef5f045 | ||
|
|
7673918178 | ||
|
|
92b0aad937 | ||
|
|
233e126cd3 | ||
|
|
e9506787d5 | ||
|
|
e061289a26 | ||
|
|
7f87164235 | ||
|
|
0627d37f69 | ||
|
|
68dc49cb88 | ||
|
|
24cf97ca84 | ||
|
|
9859cbdca6 | ||
|
|
b819f303bb | ||
|
|
5ad4a25c33 | ||
|
|
265e5bb122 | ||
|
|
7964f862c6 | ||
|
|
622176d3de | ||
|
|
7e793a48e7 | ||
|
|
5bdc4615e8 | ||
|
|
ef04a4376a | ||
|
|
3c57007765 | ||
|
|
9965f2e79c | ||
|
|
f131997d6a | ||
|
|
08b9e3c46a | ||
|
|
8afbaefa6d | ||
|
|
88a8336ae7 | ||
|
|
0c6dface18 | ||
|
|
9bb2dd3887 | ||
|
|
1f3943fe9a | ||
|
|
9502034d58 | ||
|
|
c6b0b2021c | ||
|
|
0d889ec773 | ||
|
|
3d8f848936 | ||
|
|
6cbd108bca | ||
|
|
03c950f82e | ||
|
|
552b061357 | ||
|
|
ca98432e20 | ||
|
|
6ada1f04da | ||
|
|
edf1738e3c | ||
|
|
5b77205b77 | ||
|
|
67128c2fd5 | ||
|
|
490ec7ef3e | ||
|
|
0ab7ff919b | ||
|
|
489ccd9003 | ||
|
|
9971eef9b8 | ||
|
|
88e8bed395 | ||
|
|
9304942fd6 | ||
|
|
5f3b5cbe26 | ||
|
|
c5dbc8ddca | ||
|
|
eaea041e73 | ||
|
|
20b4ae230f | ||
|
|
037dce8be8 | ||
|
|
3ad6045377 | ||
|
|
3f683c3909 | ||
|
|
2f9974f6ef | ||
|
|
d2156a1179 | ||
|
|
d6a0b92f1e | ||
|
|
15aab58415 | ||
|
|
1e8679bf94 | ||
|
|
4eeb4c515f | ||
|
|
76a75942a9 | ||
|
|
4383c3486a | ||
|
|
e629877984 | ||
|
|
fb71ea83f0 | ||
|
|
d9cf090060 | ||
|
|
a0fd16513a | ||
|
|
c7c6d4e162 | ||
|
|
9a2eaa7a1d | ||
|
|
3b12843d5f | ||
|
|
6685649e39 | ||
|
|
7e63c902d3 | ||
|
|
c1e1530cd9 | ||
|
|
b1a190093c | ||
|
|
fcc7cf0566 | ||
|
|
ca3d51157f | ||
|
|
899a7b2500 | ||
|
|
d19be1731e | ||
|
|
90768b6933 | ||
|
|
89d6f050d1 | ||
|
|
5bfe7a2146 | ||
|
|
e54dc157ac | ||
|
|
5be7472dd9 | ||
|
|
1a2de2bbc2 | ||
|
|
0c9935b1b5 | ||
|
|
aba00ad17b | ||
|
|
5ca05394d6 | ||
|
|
5165415fb1 | ||
|
|
1f1060b97c | ||
|
|
3e9dabe2a5 | ||
|
|
86d72b15c5 | ||
|
|
3fd3a3e5aa | ||
|
|
3fbcf09326 | ||
|
|
d97cd9eabe | ||
|
|
de52bb1036 | ||
|
|
f9a073fd93 | ||
|
|
c77f99cf9b | ||
|
|
af8c1b2092 | ||
|
|
9095dcd9d8 | ||
|
|
08359103da | ||
|
|
9fe15fd8c2 | ||
|
|
728afbc887 | ||
|
|
1e10d49d0f | ||
|
|
4a81ea0ee4 | ||
|
|
acf89b1e9e | ||
|
|
1c99b5bbc7 | ||
|
|
d8d0c67462 | ||
|
|
2e9f050707 | ||
|
|
4b0a3741de | ||
|
|
c45c5f6e92 | ||
|
|
482515f538 | ||
|
|
42d75bd1ee | ||
|
|
296d7436ae | ||
|
|
5ba1466b96 | ||
|
|
7cdf36df87 | ||
|
|
02f25f8816 | ||
|
|
fa7e6bfa36 | ||
|
|
854890cc77 | ||
|
|
5445ec1b8d | ||
|
|
050f11bfab | ||
|
|
da89b95654 | ||
|
|
71631ccf0c | ||
|
|
f1c38dd742 | ||
|
|
0fc7126238 | ||
|
|
41cf366014 | ||
|
|
4288ae3582 | ||
|
|
e57d0a4d0e | ||
|
|
1b6a0188f3 | ||
|
|
e225b8a77c | ||
|
|
e4b1c542db | ||
|
|
b960460e02 | ||
|
|
c8f39802ae | ||
|
|
e3caad856d | ||
|
|
97bc512d1c | ||
|
|
5752fe698f | ||
|
|
727ef87a11 | ||
|
|
a92c85a0be | ||
|
|
225790cc4b | ||
|
|
0202d52e49 | ||
|
|
397ad4c597 | ||
|
|
ce00f31f0f | ||
|
|
b9dc9e6eb0 | ||
|
|
05884bba0d | ||
|
|
d8f37ac6a7 | ||
|
|
9c89e0b790 | ||
|
|
b37d695c8b | ||
|
|
263db54a12 | ||
|
|
dc8c25b8bc | ||
|
|
1d43cae1b1 | ||
|
|
7e02ae518d | ||
|
|
4afc439eb1 | ||
|
|
330d4f006f | ||
|
|
5929a7c6f9 | ||
|
|
1b95989b05 | ||
|
|
b284fc590e | ||
|
|
dd3e21e5d4 | ||
|
|
31c674c3dc | ||
|
|
b3632417a7 | ||
|
|
bc28919727 | ||
|
|
4333744248 | ||
|
|
d62599d62c | ||
|
|
bad745d2a9 | ||
|
|
0c6aa6afdb | ||
|
|
bb8e68d1b1 | ||
|
|
8c6b6e617c | ||
|
|
a9e4270ddb | ||
|
|
72262a77fd | ||
|
|
68c524a75d | ||
|
|
64c56288c9 | ||
|
|
2996bf7e6b | ||
|
|
5520c3778b | ||
|
|
4c6bdcb68a | ||
|
|
a925786899 | ||
|
|
e73a3cbf8c | ||
|
|
c86dbb4665 | ||
|
|
951c338edd | ||
|
|
5a22ad86b8 | ||
|
|
6db22d43a6 | ||
|
|
6569f59501 | ||
|
|
c30b04131b | ||
|
|
fb7e6d482a | ||
|
|
df252a0051 | ||
|
|
443d79f902 | ||
|
|
7ac9997950 | ||
|
|
aa59b5b4e3 | ||
|
|
2cd0b56f11 | ||
|
|
40c8485f02 | ||
|
|
f70e22fa4d | ||
|
|
cbaa7028d0 | ||
|
|
d6a4e33035 | ||
|
|
99bae93e07 | ||
|
|
9097fb6f60 | ||
|
|
8e9b91eeb6 | ||
|
|
419b46c7cc | ||
|
|
e927d76cb5 | ||
|
|
966d190b77 | ||
|
|
b7b06413d6 | ||
|
|
67ba5e21f7 | ||
|
|
27e401d3a1 | ||
|
|
45c1c40e9f | ||
|
|
29cfde5249 | ||
|
|
ffecb391b3 | ||
|
|
c87f8b0986 | ||
|
|
8cc4eef916 | ||
|
|
46f241f004 | ||
|
|
4dbe0bdbcb | ||
|
|
ee5e0beb7f | ||
|
|
ef89a35de6 | ||
|
|
26f94ccf07 | ||
|
|
c7a0b9d31c | ||
|
|
e2f91339b4 | ||
|
|
155b2723f1 | ||
|
|
2b5d5c5c12 | ||
|
|
660f854546 | ||
|
|
505ae78d14 | ||
|
|
0f19934021 | ||
|
|
0865e3262f | ||
|
|
8d266dbc4c | ||
|
|
8801365896 | ||
|
|
33249ea526 | ||
|
|
d1f3ec25fc | ||
|
|
8ea76ee1b8 | ||
|
|
660ed4c341 | ||
|
|
ef6f88a129 | ||
|
|
bf072e0802 | ||
|
|
d910521a0b | ||
|
|
3ceb40741c | ||
|
|
7d3a192435 | ||
|
|
50af4b83c5 | ||
|
|
2e4f79f6ef | ||
|
|
7c57be2181 | ||
|
|
2f238789ad | ||
|
|
29d4cfe669 | ||
|
|
df4eaa28b9 | ||
|
|
42dc55c664 | ||
|
|
a82ad37e78 | ||
|
|
5fdc7970cd | ||
|
|
8f45cc62d2 | ||
|
|
b8eaa369c2 | ||
|
|
fec2a4fcd5 | ||
|
|
f40ca2037b | ||
|
|
7227885e0e | ||
|
|
4fe66ddca8 | ||
|
|
5472f17c96 | ||
|
|
69b072f130 | ||
|
|
640106a78a | ||
|
|
2e31e1840a | ||
|
|
09334d2043 | ||
|
|
b52d22fd76 | ||
|
|
34233bf682 | ||
|
|
3ca111f6e0 | ||
|
|
09556cc3ac | ||
|
|
ff94d85e53 | ||
|
|
dd759d62e9 | ||
|
|
9739ed6b87 | ||
|
|
3d4c88ae7c | ||
|
|
eb5f842e7b | ||
|
|
6fb409dcd0 | ||
|
|
b2e36e8ca2 | ||
|
|
709524b7c9 | ||
|
|
4444438670 | ||
|
|
87ada19569 | ||
|
|
082fce33c3 | ||
|
|
ae93d2028a | ||
|
|
a36719686b | ||
|
|
981dc381cf | ||
|
|
261241c1ee | ||
|
|
7c8d432aa9 | ||
|
|
bd82d5d355 | ||
|
|
fed9270211 | ||
|
|
2fa0ef2572 | ||
|
|
02ffbb2b5c | ||
|
|
c00197d44a | ||
|
|
aa30390674 | ||
|
|
c54bf6f730 | ||
|
|
a5cc4cc01d | ||
|
|
ca02c342d7 | ||
|
|
bd58d81bcc | ||
|
|
15dfdb5bc1 | ||
|
|
36730dd6e2 | ||
|
|
0122589138 | ||
|
|
e82b1c4524 | ||
|
|
8a0ae86b28 | ||
|
|
d406277247 | ||
|
|
5e5015c552 | ||
|
|
22c46a50f0 | ||
|
|
7c9404864a | ||
|
|
82752f55d1 | ||
|
|
50a7643610 | ||
|
|
a85c84096f | ||
|
|
7edda12548 | ||
|
|
d91f1df9d8 | ||
|
|
7c2f9e529c | ||
|
|
d7ca06c599 | ||
|
|
f4ee0441ea | ||
|
|
7b28ea1fa3 | ||
|
|
e02da44288 | ||
|
|
e836880a39 | ||
|
|
2ef00808b7 | ||
|
|
1dca52e8cd | ||
|
|
a4f7bd4d2b | ||
|
|
5740917a11 | ||
|
|
0790220a44 | ||
|
|
462459c6c2 | ||
|
|
eee160c379 | ||
|
|
96c1ce5803 | ||
|
|
6dc705450b | ||
|
|
e9f5039817 | ||
|
|
4a7d0222a2 | ||
|
|
63e7803ea4 | ||
|
|
c522710f7f | ||
|
|
d9be2b2437 | ||
|
|
caeaefcc6a | ||
|
|
6048161924 | ||
|
|
4348bc20eb | ||
|
|
bff6d43701 | ||
|
|
944fc70ff4 | ||
|
|
a75a148278 | ||
|
|
a378eefb7b | ||
|
|
9782705e06 | ||
|
|
4cd47da3f4 | ||
|
|
65776d8401 | ||
|
|
1c5c5b3429 | ||
|
|
284d51918f | ||
|
|
06a1a49a73 | ||
|
|
1c2c72ca7c | ||
|
|
f2a28a34fe | ||
|
|
34e6eedf23 | ||
|
|
c1042d15b5 | ||
|
|
c40f258725 | ||
|
|
8619c180de | ||
|
|
1e9e5c5956 | ||
|
|
095769b7cd | ||
|
|
0740115724 | ||
|
|
862f2080ea | ||
|
|
2ed264b2b1 | ||
|
|
e9c2ea0e87 | ||
|
|
79cea28630 | ||
|
|
ec6ed8cea5 | ||
|
|
eab7a03c77 | ||
|
|
970d82e48e | ||
|
|
a9ad5169f2 | ||
|
|
27e0b3e62f | ||
|
|
3dfa2cd397 | ||
|
|
782eca7b95 | ||
|
|
17c6e46e8d | ||
|
|
684b3d2b2b | ||
|
|
e0c4ee102c | ||
|
|
0766a4b76d | ||
|
|
e16bd40fd3 | ||
|
|
479f72f160 | ||
|
|
56bfb32ad4 | ||
|
|
45dbb08993 | ||
|
|
5030f6e901 | ||
|
|
e64003a22f | ||
|
|
3abdeee850 | ||
|
|
dfec2dc308 | ||
|
|
abede2f979 | ||
|
|
695a0dac40 | ||
|
|
266e2e13cf | ||
|
|
79a81c86bb | ||
|
|
20f7bf5ebf | ||
|
|
bc3cd33a30 | ||
|
|
5eb3902854 | ||
|
|
eb75911d50 | ||
|
|
81b352c888 | ||
|
|
4b64dff6dd | ||
|
|
ea2eb39975 | ||
|
|
c3d64a7d2a | ||
|
|
4049a66c45 | ||
|
|
b06749aff9 | ||
|
|
6fa418b43b | ||
|
|
831786f55e | ||
|
|
952a9b69e1 | ||
|
|
8d367a1167 | ||
|
|
6cb366a978 | ||
|
|
aa99373d2d | ||
|
|
a6633365ec | ||
|
|
cac0f46d1c | ||
|
|
3e3b14779e | ||
|
|
225767acab | ||
|
|
7d1f31247e | ||
|
|
5fe1de5f7f | ||
|
|
0ac4f98ffa | ||
|
|
ebd3c72170 | ||
|
|
839a91d144 | ||
|
|
5c3df787bb | ||
|
|
daeec63b71 | ||
|
|
1aea0aa24c | ||
|
|
7b53957e55 | ||
|
|
09c7af4cc4 | ||
|
|
7ed9526828 | ||
|
|
27c61f1596 | ||
|
|
ca8e4d19bf | ||
|
|
938114f942 | ||
|
|
b7c61baafb | ||
|
|
5850e271c3 | ||
|
|
7ab15776df | ||
|
|
7f0b819993 | ||
|
|
c0af1fa1eb | ||
|
|
316eb681f6 | ||
|
|
2a841e4ae2 | ||
|
|
0033de0831 | ||
|
|
f70af84e16 | ||
|
|
d9571f3c4d | ||
|
|
fda2f1ff15 | ||
|
|
0d4824b7e0 | ||
|
|
0cd376e039 | ||
|
|
101c953020 | ||
|
|
46bf18cbfa | ||
|
|
5348cd3f22 | ||
|
|
9591ed744b | ||
|
|
efed8bbd4b | ||
|
|
7e0025d3fc | ||
|
|
b016b56a30 | ||
|
|
167d577f4d | ||
|
|
8888c97e07 | ||
|
|
b99e1f7b28 | ||
|
|
39b09c5b25 | ||
|
|
86842735bf | ||
|
|
05cf67d3e1 | ||
|
|
9f10d1b923 | ||
|
|
db66c6cde4 | ||
|
|
0eaa99a91a | ||
|
|
11de057064 | ||
|
|
d206ed5c0b | ||
|
|
6dd0275e5a | ||
|
|
cefaa78832 | ||
|
|
7253c56086 | ||
|
|
38dc4274ff | ||
|
|
833a5bd974 | ||
|
|
2443772e78 | ||
|
|
243a24c7f2 | ||
|
|
1166297dda | ||
|
|
58bfc4b36c | ||
|
|
1b07c8838f | ||
|
|
6592694778 | ||
|
|
08d5735af9 | ||
|
|
4cd69a52f6 | ||
|
|
542bab816f | ||
|
|
357be5296f | ||
|
|
57816a4f3a | ||
|
|
7486eb640f | ||
|
|
e946e23360 | ||
|
|
e233eed8eb | ||
|
|
80ac8906b3 | ||
|
|
ee9cb656fc | ||
|
|
cfe9f8e6e6 | ||
|
|
72f768cca4 | ||
|
|
9d0206c432 | ||
|
|
53bc91d399 | ||
|
|
ca63763d04 | ||
|
|
ac7589a3ff | ||
|
|
d752442aa3 | ||
|
|
930c27a890 | ||
|
|
2caec7be0e | ||
|
|
a7cfe98ca3 | ||
|
|
bad5f6f431 | ||
|
|
d0116c852c | ||
|
|
3f3ce39b30 | ||
|
|
8a5692e9df | ||
|
|
5cf96b1154 | ||
|
|
2d392de9ef | ||
|
|
f53aff1882 | ||
|
|
0cf6cbf3bc | ||
|
|
28b428a6f1 | ||
|
|
d311ced441 | ||
|
|
3ccce18f23 | ||
|
|
0ecd719799 | ||
|
|
5438e20b01 | ||
|
|
b5f72ecc5c | ||
|
|
dd20c8caf0 | ||
|
|
c3b96e4f68 | ||
|
|
b3132aa757 | ||
|
|
7685e3233f | ||
|
|
ee3e66580d | ||
|
|
26cd2acbcd | ||
|
|
0c3b099265 | ||
|
|
6971d320ae | ||
|
|
2dd6305e54 | ||
|
|
baf5479d0b | ||
|
|
c57be04143 | ||
|
|
f1030c2f10 | ||
|
|
1f1639e661 | ||
|
|
04d3445834 | ||
|
|
1559a14e39 | ||
|
|
fbbc6aa005 | ||
|
|
9c573b9bfd | ||
|
|
96bbe2c7dc | ||
|
|
ec9f6b82d9 | ||
|
|
2c120c3c45 | ||
|
|
b911f5c45b | ||
|
|
6366a22ff3 | ||
|
|
5fc819d8f5 | ||
|
|
df9d1ddcbf | ||
|
|
eed7b282d3 | ||
|
|
fc6237cd29 | ||
|
|
c473e634e0 | ||
|
|
28866fc1dc | ||
|
|
8028e8ca12 | ||
|
|
28d1cc2055 | ||
|
|
cffd99b68d | ||
|
|
ac92a92090 | ||
|
|
28dbb309cf | ||
|
|
7b585aaa05 | ||
|
|
82f0c85bd0 | ||
|
|
1c0a7d8475 | ||
|
|
5f63b5f907 | ||
|
|
7459cd65d9 | ||
|
|
0b61ddaff7 | ||
|
|
d554a7ee97 | ||
|
|
3d1e130b1e | ||
|
|
a63e008f84 | ||
|
|
d76ae27611 | ||
|
|
bf00d79e23 | ||
|
|
c08775f3e3 | ||
|
|
d94ceaf492 | ||
|
|
9802cb62d9 | ||
|
|
2c4f90b31c | ||
|
|
163d95800b | ||
|
|
0662d44a86 | ||
|
|
c3792191a2 | ||
|
|
97ff18c3e2 | ||
|
|
d069252f76 | ||
|
|
54fda151be | ||
|
|
04c4cf4426 | ||
|
|
7c81fe3339 | ||
|
|
330862e6c0 | ||
|
|
ce309a384e | ||
|
|
6bc01c3472 | ||
|
|
9ecf3e11bf | ||
|
|
7da306eb65 | ||
|
|
3d9d6d45fe | ||
|
|
7fc85a47be | ||
|
|
6accf7fd6c | ||
|
|
f619155667 | ||
|
|
89d50af521 | ||
|
|
fe3a111d6f | ||
|
|
75b6b6316b | ||
|
|
f64822a18b | ||
|
|
e566fb2d57 | ||
|
|
9f5931509c | ||
|
|
deb7ee8c3a | ||
|
|
6a56e29474 | ||
|
|
c79ccd7e23 | ||
|
|
36e395e3a3 | ||
|
|
c0b106c7ef | ||
|
|
23c8c5da19 | ||
|
|
883f1a7cf8 | ||
|
|
b2ff43bd9b | ||
|
|
471bb1cacd | ||
|
|
de30329a58 | ||
|
|
c990c8af56 | ||
|
|
2dbd099c68 | ||
|
|
25da363d56 | ||
|
|
a957566fc9 | ||
|
|
97db42427e | ||
|
|
34fabaab5f | ||
|
|
11893e244e | ||
|
|
24f1285a43 | ||
|
|
2d2e0fd6c0 | ||
|
|
5ea2aca8a8 | ||
|
|
088b4fa07e | ||
|
|
7dcfbc409c | ||
|
|
44bb2ff0f5 | ||
|
|
4effb02219 | ||
|
|
5d7ad6bb81 | ||
|
|
2bef5d7653 | ||
|
|
6b76bec480 | ||
|
|
76ab53078e | ||
|
|
ddf8df9c71 | ||
|
|
b312f28ab6 | ||
|
|
d9e536b789 | ||
|
|
2e768cb77b | ||
|
|
d611dbd386 | ||
|
|
8976e238a5 | ||
|
|
eb13bb8b6d | ||
|
|
d72eba1602 | ||
|
|
cd4685c093 | ||
|
|
bc3aa56ed9 | ||
|
|
903ead9e91 | ||
|
|
bacbd8baf3 | ||
|
|
407c742124 | ||
|
|
34a91b7e3d | ||
|
|
65f4724f87 | ||
|
|
afafe46221 | ||
|
|
ff220113af | ||
|
|
81c2c482cd | ||
|
|
afb06baa56 | ||
|
|
d25ff61f22 | ||
|
|
067f0b3a66 | ||
|
|
0f71efd1e9 | ||
|
|
351f9c1976 | ||
|
|
a11eff4d9a | ||
|
|
fe6a5a700a | ||
|
|
87a994b8ce | ||
|
|
3f06553604 | ||
|
|
e9f765a216 | ||
|
|
532e65f3a0 | ||
|
|
b038044347 | ||
|
|
0664d11f55 | ||
|
|
6d00a26530 | ||
|
|
a4d28b13ef | ||
|
|
fd01dc1bb1 | ||
|
|
fa72d3d0de | ||
|
|
f935f57d23 | ||
|
|
fbf1e855ad | ||
|
|
50a6037a6d | ||
|
|
d15d27defb | ||
|
|
7952bdcb48 | ||
|
|
dfdf3160d3 | ||
|
|
ce4f12179c | ||
|
|
491da787b4 | ||
|
|
6dbc7e2704 | ||
|
|
2a39c917ee | ||
|
|
3733ba45e2 | ||
|
|
2a410b9099 | ||
|
|
3ccc7c41e7 | ||
|
|
d03bc797fc | ||
|
|
5c4f8eb526 | ||
|
|
973464f1c4 | ||
|
|
e090ab2825 | ||
|
|
0413e5ded8 | ||
|
|
51c24f6fdf | ||
|
|
d1de248894 | ||
|
|
fdf3e618c2 | ||
|
|
5b0a228f4f | ||
|
|
9db11ce690 | ||
|
|
f9bdf6e181 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -32,6 +32,8 @@ bin
|
||||
gen
|
||||
*.log
|
||||
|
||||
# Ignore macOS Spotlight rubbish
|
||||
.DS_Store
|
||||
|
||||
# TODO: specify what these ignores are for (releasing?)
|
||||
|
||||
|
||||
104
README.md
104
README.md
@@ -1,15 +1,15 @@
|
||||
# Forge
|
||||
|
||||
Gitlab repo is found [here](https://git.cardforge.org/core-developers/forge).
|
||||
[Official GitLab repo](https://git.cardforge.org/core-developers/forge).
|
||||
|
||||
Dev instructions here: [Getting Started](https://www.slightlymagic.net/wiki/Forge:How_to_Get_Started_Developing_Forge) (Somewhat outdated)
|
||||
Dev instructions here: [Getting Started](https://git.cardforge.org/core-developers/forge/-/wikis/(SM-autoconverted)-how-to-get-started-developing-forge) (Somewhat outdated)
|
||||
|
||||
Discord channel [here](https://discordapp.com/channels/267367946135928833/267742313390931968)
|
||||
|
||||
# Requirements / Tools
|
||||
## Requirements / Tools
|
||||
|
||||
- Java IDE such as IntelliJ or Eclipse
|
||||
- Java JDK 8 or later
|
||||
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
|
||||
- Java JDK 8 or later (some IDEs such as Eclipse require JDK11+, whereas the Android build currently only works with JDK8)
|
||||
- Git
|
||||
- Git client (optional)
|
||||
- Maven
|
||||
@@ -18,7 +18,7 @@ Discord channel [here](https://discordapp.com/channels/267367946135928833/267742
|
||||
- Android SDK (optional: for Android releases)
|
||||
- RoboVM (optional: for iOS releases) (TBD: Current status of support by libgdx)
|
||||
|
||||
# Project Quick Setup
|
||||
## Project Quick Setup
|
||||
|
||||
- Log in to gitlab with your user account and fork the project.
|
||||
|
||||
@@ -26,18 +26,18 @@ Discord channel [here](https://discordapp.com/channels/267367946135928833/267742
|
||||
|
||||
- Go to the project location on your machine. Run Maven to download all dependencies and build a snapshot. Example for Windows & Linux: `mvn -U -B clean -P windows-linux install`
|
||||
|
||||
# Eclipse
|
||||
## Eclipse
|
||||
|
||||
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
|
||||
|
||||
## Project Setup
|
||||
### Project Setup
|
||||
|
||||
- Follow the instructions for cloning from Gitlab. You'll need a Gitlab account setup and an SSH key defined.
|
||||
- Follow the instructions for cloning from Gitlab. You'll need a Gitlab account setup and an SSH key defined.
|
||||
|
||||
If you are on a Windows machine you can use Putty with TortoiseGit for SSH keys. Run puttygen.exe to generate the key -- save the private key and export
|
||||
the OpenSSH public key. If you just leave the dialog open, you can copy and paste the key from it to your Gitlab profile under
|
||||
"SSH keys". Run pageant.exe and add the private key generated earlier. TortoiseGit will use this for accessing Gitlab.
|
||||
|
||||
|
||||
- Fork the Forge git repo to your Gitlab account.
|
||||
|
||||
- Clone your forked repo to your local machine.
|
||||
@@ -48,16 +48,16 @@ Eclipse includes Maven integration so a separate install is not necessary. For
|
||||
|
||||
- Create a workspace. Go to the workbench. Right-click inside of Package Explorer > Import... > Maven > Existing Maven Projects > Navigate to root path of the local forge repo and
|
||||
ensure everything is checked > Finish.
|
||||
|
||||
|
||||
- Let Eclipse run through building the project. You may be prompted for resolving any missing Maven plugins -- accept the ones offered. You may see errors appear in the "Problems" tab. These should
|
||||
be automatically resolved as plug-ins are installed and Eclipse continues the build process. If this is the first time for some plug-in installs, Eclipse may prompt you to restart. Do so. Be patient
|
||||
for this first time through.
|
||||
|
||||
|
||||
- Once everything builds, all errors should disappear. You can now advance to Project launch.
|
||||
|
||||
## Project Launch
|
||||
### Project Launch
|
||||
|
||||
### Desktop
|
||||
#### Desktop
|
||||
|
||||
This is the standard configuration used for releasing to Windows / Linux / MacOS.
|
||||
|
||||
@@ -65,7 +65,7 @@ This is the standard configuration used for releasing to Windows / Linux / MacOS
|
||||
|
||||
- The familiar Forge splash screen, etc. should appear. Enjoy!
|
||||
|
||||
### Mobile (Desktop dev)
|
||||
#### Mobile (Desktop dev)
|
||||
|
||||
This is the configuration used for doing mobile development using the Windows / Linux / MacOS front-end. Knowledge of libgdx is helpful here.
|
||||
|
||||
@@ -73,50 +73,49 @@ This is the configuration used for doing mobile development using the Windows /
|
||||
|
||||
- A view similar to a mobile phone should appear. Enjoy!
|
||||
|
||||
## Eclipse / Android SDK Integration
|
||||
### Eclipse / Android SDK Integration
|
||||
|
||||
Google no longer supports Android SDK releases for Eclipse. That said, it is still possible to build and debug Android platforms.
|
||||
|
||||
### Android SDK
|
||||
#### Android SDK
|
||||
|
||||
Reference SO for obtaining a specific release: https://stackoverflow.com/questions/27043522/where-can-i-download-an-older-version-of-the-android-sdk
|
||||
|
||||
#### Windows
|
||||
##### Windows
|
||||
|
||||
Download the following archived version of the Android SDK: http://dl-ssl.google.com/android/repository/tools_r25.2.3-windows.zip. Install it somewhere on your machine. This is referenced
|
||||
in the following instructions as your 'Android SDK Install' path.
|
||||
|
||||
#### Linux / Mac OSX
|
||||
##### Linux / Mac OSX
|
||||
|
||||
TBD
|
||||
|
||||
### Android Plugin for Eclipse
|
||||
#### Android Plugin for Eclipse
|
||||
|
||||
Google's last plugin release does not work completely with target's running Android 7.0 or later. Download the ADT-24.2.0-20160729.zip plugin
|
||||
from: https://github.com/khaledev/ADT/releases
|
||||
Google's last plugin release does not work completely with target's running Android 7.0 or later. Download the ADT-24.2.0-20160729.zip plugin
|
||||
from: https://github.com/khaledev/ADT/releases
|
||||
|
||||
In Eclipse go to: Help > Install New Software... > Add > Name: ADT Update, Click on the "Archive:" button and navigate to the downloaded ADT-24.2.0-20160729.zip file > Add. Install all "Developer Tools". Eclipse
|
||||
should restart and prompt you to run the SDK Manager. Launch it and continue to the next steps below.
|
||||
|
||||
### Android Platform
|
||||
#### Android Platform
|
||||
|
||||
In Eclipse, if the SDK Manager is not already running, go to Window > Android SDK Manager. Install the following options / versions:
|
||||
|
||||
- Android SDK Build-tools 26.0.1
|
||||
- Android 7.1.1 (API 25) SDK Platform
|
||||
- Google USB Driver 11
|
||||
- Android 8.0.0 (API 26) SDK Platform
|
||||
- Google USB Driver (in case your phone is not detected by ADB)
|
||||
|
||||
Note that this will populate additional tools in the Android SDK install path extracted above.
|
||||
|
||||
### Proguard update
|
||||
#### Proguard update
|
||||
|
||||
The Proguard included with the Android SDK Build-tools is outdated and does not work with Java 1.8. Download Proguard 6.0.3 from https://sourceforge.net/projects/proguard/files/proguard/6.0/.
|
||||
The Proguard included with the Android SDK Build-tools is outdated and does not work with Java 1.8. Download Proguard 6.0.3 or later (last tested with 7.0.1) from https://github.com/Guardsquare/proguard
|
||||
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard-4.7/.
|
||||
|
||||
- Go to the Android SDK install path. Rename the tools/proguard/ path to tools/proguard4.7/.
|
||||
- Extract your Proguard version to the Android SDK install path under tools/. You will need to either rename the dir proguard-<your-version> to proguard/ or, if your filesystem supports it, use a symbolic link (the later is highly recommended), such as `ln -s proguard proguard-<your-version>`.
|
||||
|
||||
- Extract Proguard 6.0.3 to the Android SDK install path under tools/. You will need to rename the dir proguard6.0.3/ to proguard/.
|
||||
|
||||
### Android Build
|
||||
#### Android Build
|
||||
|
||||
The Eclipse plug-ins do NOT support building things for Android. They do however allow you to use the debugger so you can still set breakpoints and trace
|
||||
things out. The steps below show how to generate a debug Android build.
|
||||
@@ -135,7 +134,7 @@ things out. The steps below show how to generate a debug Android build.
|
||||
|
||||
Assuming you got this far, you should have an Android forge-android-[version].apk in the forge-gui-android/target path.
|
||||
|
||||
### Android Deploy
|
||||
#### Android Deploy
|
||||
|
||||
You'll need to have the Android SDK install path platform-tools/ path in your command search path to easily deploy builds.
|
||||
|
||||
@@ -149,37 +148,37 @@ You'll need to have the Android SDK install path platform-tools/ path in your co
|
||||
|
||||
- Install the new apk: `adb install forge-android-[version].apk`
|
||||
|
||||
### Android Debugging
|
||||
#### Android Debugging
|
||||
|
||||
Assuming the apk is installed, launch it from the device.
|
||||
|
||||
In Eclipse, launch the DDMS. Window > Perspective > Open Perspective > Other... > DDMS. You should see the forge app in the list. Highlight the app, click on the green debug button and a
|
||||
In Eclipse, launch the DDMS. Window > Perspective > Open Perspective > Other... > DDMS. You should see the forge app in the list. Highlight the app, click on the green debug button and a
|
||||
green debug button should appear next to the app's name. You can now set breakpoints and step through the source code.
|
||||
|
||||
## Windows / Linux SNAPSHOT build
|
||||
### Windows / Linux SNAPSHOT build
|
||||
|
||||
SNAPSHOT builds can be built via the Maven integration in Eclipse.
|
||||
|
||||
1) Create a Maven build for the forge top-level project. Right-click on the forge project. Run as.. > Maven build...
|
||||
- On the Main tab, set Goals: clean install, set Profiles: windows-linux
|
||||
|
||||
|
||||
2) Run forge Maven build. If everything built, you should see "BUILD SUCCESS" in the Console View.
|
||||
|
||||
The resulting snapshot will be found at: forge-gui-desktop/target/forge-gui-desktop-[version]-SNAPSHOT
|
||||
|
||||
# IntelliJ
|
||||
## IntelliJ
|
||||
|
||||
Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/wikis/intellij-setup).
|
||||
Quick start guide for [setting up the Forge project within IntelliJ](https://git.cardforge.org/core-developers/forge/-/wikis/Development/intellij-setup).
|
||||
|
||||
# Card Scripting
|
||||
## Card Scripting
|
||||
|
||||
Visit [this page](https://www.slightlymagic.net/wiki/Forge_API) for information on scripting.
|
||||
Visit [this page](https://git.cardforge.org/core-developers/forge/-/wikis/Card-scripting-API/Card-scripting-API) for information on scripting.
|
||||
|
||||
Card scripting resources are found in the forge-gui/res/ path.
|
||||
|
||||
# General Notes
|
||||
## General Notes
|
||||
|
||||
## Project Hierarchy
|
||||
### Project Hierarchy
|
||||
|
||||
Forge is divided into 4 primary projects with additional projects that target specific platform releases. The primary projects are:
|
||||
|
||||
@@ -196,35 +195,34 @@ The platform-specific projects are:
|
||||
- forge-gui-mobile
|
||||
- forge-gui-mobile-dev
|
||||
|
||||
### forge-ai
|
||||
#### forge-ai
|
||||
|
||||
### forge-core
|
||||
#### forge-core
|
||||
|
||||
### forge-game
|
||||
#### forge-game
|
||||
|
||||
### forge-gui
|
||||
#### forge-gui
|
||||
|
||||
The forge-gui project includes the scripting resource definitions in the res/ path.
|
||||
|
||||
### forge-gui-android
|
||||
#### forge-gui-android
|
||||
|
||||
Libgdx-based backend targeting Android. Requires Android SDK and relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
### forge-gui-desktop
|
||||
#### forge-gui-desktop
|
||||
|
||||
Java Swing based GUI targeting desktop machines.
|
||||
Java Swing based GUI targeting desktop machines.
|
||||
|
||||
Screen layout and game logic revolving around the GUI is found here. For example, the overlay arrows (when enabled) that indicate attackers and blockers, or the targets of the stack are defined and drawn by this.
|
||||
|
||||
### forge-gui-ios
|
||||
#### forge-gui-ios
|
||||
|
||||
Libgdx-based backend targeting iOS. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
### forge-gui-mobile
|
||||
#### forge-gui-mobile
|
||||
|
||||
Mobile GUI game logic utilizing [libgdx](https://libgdx.badlogicgames.com/) library. Screen layout and game logic revolving around the GUI for the mobile platforms is found here.
|
||||
|
||||
### forge-gui-mobile-dev
|
||||
#### forge-gui-mobile-dev
|
||||
|
||||
Libgdx backend for desktop development for mobile backends. Utilizes LWJGL. Relies on forge-gui-mobile for GUI logic.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.40-SNAPSHOT</version>
|
||||
<version>1.6.41</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-ai</artifactId>
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
package forge.ai;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
@@ -43,6 +44,7 @@ import forge.game.combat.GlobalAttackRestrictions;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
@@ -54,7 +56,6 @@ import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
|
||||
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
|
||||
/**
|
||||
* <p>
|
||||
* ComputerUtil_Attack2 class.
|
||||
@@ -91,7 +92,7 @@ public class AiAttackController {
|
||||
|
||||
public AiAttackController(final Player ai, boolean nextTurn) {
|
||||
this.ai = ai;
|
||||
this.defendingOpponent = choosePreferredDefenderPlayer();
|
||||
this.defendingOpponent = choosePreferredDefenderPlayer(ai);
|
||||
this.oppList = getOpponentCreatures(this.defendingOpponent);
|
||||
this.myList = ai.getCreaturesInPlay();
|
||||
this.attackers = new ArrayList<>();
|
||||
@@ -107,7 +108,7 @@ public class AiAttackController {
|
||||
|
||||
public AiAttackController(final Player ai, Card attacker) {
|
||||
this.ai = ai;
|
||||
this.defendingOpponent = choosePreferredDefenderPlayer();
|
||||
this.defendingOpponent = choosePreferredDefenderPlayer(ai);
|
||||
this.oppList = getOpponentCreatures(this.defendingOpponent);
|
||||
this.myList = ai.getCreaturesInPlay();
|
||||
this.attackers = new ArrayList<>();
|
||||
@@ -156,13 +157,12 @@ public class AiAttackController {
|
||||
}
|
||||
|
||||
/** Choose opponent for AI to attack here. Expand as necessary. */
|
||||
private Player choosePreferredDefenderPlayer() {
|
||||
Player defender = ai.getWeakestOpponent(); //Gets opponent with the least life
|
||||
public static Player choosePreferredDefenderPlayer(Player ai) {
|
||||
Player defender = ai.getWeakestOpponent(); //Concentrate on opponent within easy kill range
|
||||
|
||||
if (defender.getLife() < 8) { //Concentrate on opponent within easy kill range
|
||||
return defender;
|
||||
} else { //Otherwise choose a random opponent to ensure no ganging up on players
|
||||
defender = ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size()));
|
||||
if (defender.getLife() > 8) { //Otherwise choose a random opponent to ensure no ganging up on players
|
||||
// TODO should we cache the random for each turn? some functions like shouldPumpCard base their decisions on the assumption who will be attacked
|
||||
return ai.getOpponents().get(MyRandom.getRandom().nextInt(ai.getOpponents().size()));
|
||||
}
|
||||
return defender;
|
||||
}
|
||||
@@ -393,7 +393,7 @@ public class AiAttackController {
|
||||
|
||||
//Calculate the amount of creatures necessary
|
||||
for (int i = 0; i < list.size(); i++) {
|
||||
if (!this.doesHumanAttackAndWin(ai, i)) {
|
||||
if (!doesHumanAttackAndWin(ai, i)) {
|
||||
blockersNeeded = i;
|
||||
break;
|
||||
}
|
||||
@@ -413,12 +413,10 @@ public class AiAttackController {
|
||||
|
||||
final Player opp = this.defendingOpponent;
|
||||
|
||||
// Increase the total number of blockers needed by 1 if Finest Hour in
|
||||
// play
|
||||
// Increase the total number of blockers needed by 1 if Finest Hour in play
|
||||
// (human will get an extra first attack with a creature that untaps)
|
||||
// In addition, if the computer guesses it needs no blockers, make sure
|
||||
// that
|
||||
// it won't be surprised by Exalted
|
||||
// that it won't be surprised by Exalted
|
||||
final int humanExaltedBonus = opp.countExaltedBonus();
|
||||
|
||||
if (humanExaltedBonus > 0) {
|
||||
@@ -428,8 +426,7 @@ public class AiAttackController {
|
||||
// total attack = biggest creature + exalted, *2 if Rafiq is in play
|
||||
int humanBasePower = getAttack(this.oppList.get(0)) + humanExaltedBonus;
|
||||
if (finestHour) {
|
||||
// For Finest Hour, one creature could attack and get the
|
||||
// bonus TWICE
|
||||
// For Finest Hour, one creature could attack and get the bonus TWICE
|
||||
humanBasePower = humanBasePower + humanExaltedBonus;
|
||||
}
|
||||
final int totalExaltedAttack = opp.isCardInPlay("Rafiq of the Many") ? 2 * humanBasePower
|
||||
@@ -450,7 +447,6 @@ public class AiAttackController {
|
||||
return notNeededAsBlockers;
|
||||
}
|
||||
|
||||
// this uses a global variable, which isn't perfect
|
||||
public final boolean doesHumanAttackAndWin(final Player ai, final int nBlockingCreatures) {
|
||||
int totalAttack = 0;
|
||||
int totalPoison = 0;
|
||||
@@ -624,7 +620,7 @@ public class AiAttackController {
|
||||
int totalCombatDamage = ComputerUtilCombat.sumDamageIfUnblocked(unblockedAttackers, opp) + trampleDamage;
|
||||
int totalPoisonDamage = ComputerUtilCombat.sumPoisonIfUnblocked(unblockedAttackers, opp);
|
||||
|
||||
if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai) >= opp.getLife()
|
||||
if (totalCombatDamage + ComputerUtil.possibleNonCombatDamage(ai, opp) >= opp.getLife()
|
||||
&& !((opp.cantLoseForZeroOrLessLife() || ai.cantWin()) && opp.getLife() < 1)) {
|
||||
return true;
|
||||
}
|
||||
@@ -641,7 +637,7 @@ public class AiAttackController {
|
||||
if (defs.size() == 1) {
|
||||
return defs.getFirst();
|
||||
}
|
||||
Player prefDefender = (Player) (defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0));
|
||||
GameEntity prefDefender = defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0);
|
||||
|
||||
// Attempt to see if there's a defined entity that must be attacked strictly this turn...
|
||||
GameEntity entity = ai.getMustAttackEntityThisTurn();
|
||||
@@ -665,7 +661,7 @@ public class AiAttackController {
|
||||
// 2. attack planeswalkers
|
||||
List<Card> pwDefending = c.getDefendingPlaneswalkers();
|
||||
if (!pwDefending.isEmpty()) {
|
||||
return pwDefending.get(0);
|
||||
return ComputerUtilCard.getBestPlaneswalkerToDamage(pwDefending);
|
||||
} else {
|
||||
return prefDefender;
|
||||
}
|
||||
@@ -682,7 +678,6 @@ public class AiAttackController {
|
||||
* @return a {@link forge.game.combat.Combat} object.
|
||||
*/
|
||||
public final void declareAttackers(final Combat combat) {
|
||||
|
||||
if (this.attackers.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
@@ -702,14 +697,14 @@ public class AiAttackController {
|
||||
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
|
||||
}
|
||||
|
||||
final boolean bAssault = this.doAssault(ai);
|
||||
final boolean bAssault = doAssault(ai);
|
||||
// TODO: detect Lightmine Field by presence of a card with a specific trigger
|
||||
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
|
||||
// TODO: detect Season of the Witch by presence of a card with a specific trigger
|
||||
final boolean seasonOfTheWitch = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Season of the Witch");
|
||||
|
||||
// Determine who will be attacked
|
||||
GameEntity defender = this.chooseDefender(combat, bAssault);
|
||||
GameEntity defender = chooseDefender(combat, bAssault);
|
||||
List<Card> attackersLeft = new ArrayList<>(this.attackers);
|
||||
|
||||
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
|
||||
@@ -717,7 +712,7 @@ public class AiAttackController {
|
||||
int attackMax = restrict.getMax();
|
||||
if (attackMax == -1) {
|
||||
// check with the local limitations vs. the chosen defender
|
||||
attackMax = ComputerUtilCombat.getMaxAttackersFor(defender);
|
||||
attackMax = restrict.getDefenderMax().get(defender) == null ? -1 : restrict.getDefenderMax().get(defender);
|
||||
}
|
||||
|
||||
if (attackMax == 0) {
|
||||
@@ -777,7 +772,6 @@ public class AiAttackController {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
if (bAssault) {
|
||||
if (LOG_AI_ATTACKS)
|
||||
System.out.println("Assault");
|
||||
@@ -851,7 +845,6 @@ public class AiAttackController {
|
||||
// no more creatures to attack
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// *******************
|
||||
// Evaluate the creature forces
|
||||
@@ -919,7 +912,7 @@ public class AiAttackController {
|
||||
// find the potential damage ratio the AI can cause
|
||||
double humanLifeToDamageRatio = 1000000;
|
||||
if (candidateUnblockedDamage > 0) {
|
||||
humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai)) / candidateUnblockedDamage;
|
||||
humanLifeToDamageRatio = (double) (opp.getLife() - ComputerUtil.possibleNonCombatDamage(ai, opp)) / candidateUnblockedDamage;
|
||||
}
|
||||
|
||||
// determine if the ai outnumbers the player
|
||||
@@ -936,12 +929,9 @@ public class AiAttackController {
|
||||
|
||||
// *********************
|
||||
// if outnumber and superior ratio work out whether attritional all out
|
||||
// attacking will work
|
||||
// attritional attack will expect some creatures to die but to achieve
|
||||
// victory by sheer weight
|
||||
// of numbers attacking turn after turn. It's not calculate very
|
||||
// carefully, the accuracy
|
||||
// can probably be improved
|
||||
// attacking will work attritional attack will expect some creatures to die but to achieve
|
||||
// victory by sheer weight of numbers attacking turn after turn. It's not calculate very
|
||||
// carefully, the accuracy can probably be improved
|
||||
// *********************
|
||||
boolean doAttritionalAttack = false;
|
||||
// get list of attackers ordered from low power to high
|
||||
@@ -975,7 +965,6 @@ public class AiAttackController {
|
||||
doAttritionalAttack = true;
|
||||
}
|
||||
}
|
||||
// System.out.println(doAttritionalAttack + " = do attritional attack");
|
||||
// *********************
|
||||
// end attritional attack calculation
|
||||
// *********************
|
||||
@@ -1025,11 +1014,9 @@ public class AiAttackController {
|
||||
// end see how long until unblockable attackers will be fatal
|
||||
// *****************
|
||||
|
||||
|
||||
// decide on attack aggression based on a comparison of forces, life
|
||||
// totals and other considerations
|
||||
// some bad "magic numbers" here, TODO replace with nice descriptive
|
||||
// variable names
|
||||
// totals and other considerations some bad "magic numbers" here
|
||||
// TODO replace with nice descriptive variable names
|
||||
if (ratioDiff > 0 && doAttritionalAttack) {
|
||||
this.aiAggression = 5; // attack at all costs
|
||||
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|
||||
@@ -1105,17 +1092,17 @@ public class AiAttackController {
|
||||
// if enough damage: switch to next planeswalker or player
|
||||
if (damage >= pw.getCounters(CounterEnumType.LOYALTY)) {
|
||||
List<Card> pwDefending = combat.getDefendingPlaneswalkers();
|
||||
boolean found = false;
|
||||
// look for next planeswalker
|
||||
for (Card walker : pwDefending) {
|
||||
if (combat.getAttackersOf(walker).isEmpty()) {
|
||||
defender = walker;
|
||||
found = true;
|
||||
break;
|
||||
for (Card walker : Lists.newArrayList(pwDefending)) {
|
||||
if (!combat.getAttackersOf(walker).isEmpty()) {
|
||||
pwDefending.remove(walker);
|
||||
}
|
||||
}
|
||||
if (!found) {
|
||||
defender = combat.getDefendingPlayers().get(0);
|
||||
if (pwDefending.isEmpty()) {
|
||||
defender = Collections.min(Lists.newArrayList(combat.getDefendingPlayers()), PlayerPredicates.compareByLife());
|
||||
}
|
||||
else {
|
||||
defender = ComputerUtilCard.getBestPlaneswalkerToDamage(pwDefending);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1224,9 +1211,8 @@ public class AiAttackController {
|
||||
}
|
||||
|
||||
// look at the attacker in relation to the blockers to establish a
|
||||
// number of factors about the attacking
|
||||
// context that will be relevant to the attackers decision according to
|
||||
// the selected strategy
|
||||
// number of factors about the attacking context that will be relevant
|
||||
// to the attackers decision according to the selected strategy
|
||||
for (final Card defender : validBlockers) {
|
||||
// if both isWorthLessThanAllKillers and canKillAllDangerous are false there's nothing more to check
|
||||
if (isWorthLessThanAllKillers || canKillAllDangerous || numberOfPossibleBlockers < 2) {
|
||||
@@ -1300,14 +1286,11 @@ public class AiAttackController {
|
||||
}
|
||||
|
||||
if (numberOfPossibleBlockers > 2
|
||||
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, combat))
|
||||
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, combat))) {
|
||||
|| (numberOfPossibleBlockers >= 1 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 1, this.defendingOpponent))
|
||||
|| (numberOfPossibleBlockers == 2 && CombatUtil.canAttackerBeBlockedWithAmount(attacker, 2, this.defendingOpponent))) {
|
||||
canBeBlocked = true;
|
||||
}
|
||||
/*System.out.println(attacker + " canBeKilledByOne: " + canBeKilledByOne + " canKillAll: "
|
||||
+ canKillAll + " isWorthLessThanAllKillers: " + isWorthLessThanAllKillers + " canBeBlocked: " + canBeBlocked);*/
|
||||
// decide if the creature should attack based on the prevailing strategy
|
||||
// choice in aiAggression
|
||||
// decide if the creature should attack based on the prevailing strategy choice in aiAggression
|
||||
switch (this.aiAggression) {
|
||||
case 6: // Exalted: expecting to at least kill a creature of equal value or not be blocked
|
||||
if ((canKillAll && isWorthLessThanAllKillers) || !canBeBlocked) {
|
||||
@@ -1373,8 +1356,7 @@ public class AiAttackController {
|
||||
}
|
||||
|
||||
// if card has a Exert Trigger which would target,
|
||||
// but there are no creatures it can target, no need to exert with
|
||||
// it
|
||||
// but there are no creatures it can target, no need to exert with it
|
||||
boolean missTarget = false;
|
||||
for (Trigger t : c.getTriggers()) {
|
||||
if (!TriggerType.Exerted.equals(t.getMode())) {
|
||||
@@ -1442,7 +1424,7 @@ public class AiAttackController {
|
||||
public String toProtectAttacker(SpellAbility sa) {
|
||||
//AiAttackController is created with the selected attacker as the only entry in "attackers"
|
||||
if (sa.getApi() != ApiType.Protection || oppList.isEmpty() || getPossibleBlockers(oppList, attackers).isEmpty()) {
|
||||
return null; //not protection sa or attacker is already unblockable
|
||||
return null; //not protection sa or attacker is already unblockable
|
||||
}
|
||||
final List<String> choices = ProtectEffect.getProtectionList(sa);
|
||||
String color = ComputerUtilCard.getMostProminentColor(getPossibleBlockers(oppList, attackers)), artifact = null;
|
||||
@@ -1452,7 +1434,7 @@ public class AiAttackController {
|
||||
if (!choices.contains(color)) {
|
||||
color = null;
|
||||
}
|
||||
for (Card c : oppList) { //find a blocker that ignores the currently selected protection
|
||||
for (Card c : oppList) { //find a blocker that ignores the currently selected protection
|
||||
if (artifact != null && !c.isArtifact()) {
|
||||
artifact = null;
|
||||
}
|
||||
@@ -1485,7 +1467,7 @@ public class AiAttackController {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (color == null && artifact == null) { //nothing can make the attacker unblockable
|
||||
if (color == null && artifact == null) { //nothing can make the attacker unblockable
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1563,4 +1545,4 @@ public class AiAttackController {
|
||||
return true;
|
||||
}
|
||||
|
||||
} // end class ComputerUtil_Attack2
|
||||
}
|
||||
|
||||
@@ -180,11 +180,9 @@ public class AiBlockController {
|
||||
|
||||
// Good Blocks means a good trade or no trade
|
||||
private void makeGoodBlocks(final Combat combat) {
|
||||
|
||||
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
||||
|
||||
for (final Card attacker : attackersLeft) {
|
||||
|
||||
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")
|
||||
|| attacker.hasKeyword(Keyword.MENACE)) {
|
||||
@@ -192,7 +190,6 @@ public class AiBlockController {
|
||||
}
|
||||
|
||||
Card blocker = null;
|
||||
|
||||
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||
|
||||
final List<Card> safeBlockers = getSafeBlockers(combat, attacker, blockers);
|
||||
@@ -305,7 +302,6 @@ public class AiBlockController {
|
||||
}
|
||||
|
||||
Card blocker = null;
|
||||
|
||||
final List<Card> blockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
|
||||
|
||||
for (Card b : blockers) {
|
||||
@@ -366,10 +362,9 @@ public class AiBlockController {
|
||||
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
|
||||
// if the total damage of the blockgang was not enough
|
||||
// without but is enough with this blocker finish the
|
||||
// blockgang
|
||||
// without but is enough with this blocker finish the blockgang
|
||||
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) < damageNeeded
|
||||
|| CombatUtil.needsBlockers(attacker) > blockGang.size()) {
|
||||
|| CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size()) {
|
||||
blockGang.add(blocker);
|
||||
if (ComputerUtilCombat.totalFirstStrikeDamageOfBlockers(attacker, blockGang) >= damageNeeded) {
|
||||
currentAttackers.remove(attacker);
|
||||
@@ -407,7 +402,7 @@ public class AiBlockController {
|
||||
boolean foundDoubleBlock = false; // if true, a good double block is found
|
||||
|
||||
// AI can't handle good blocks with more than three creatures yet
|
||||
if (CombatUtil.needsBlockers(attacker) > (considerTripleBlock ? 3 : 2)) {
|
||||
if (CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > (considerTripleBlock ? 3 : 2)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -444,7 +439,7 @@ public class AiBlockController {
|
||||
final int addedValue = ComputerUtilCard.evaluateCreature(blocker);
|
||||
final int damageNeeded = ComputerUtilCombat.getDamageToKill(attacker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, false);
|
||||
if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size())
|
||||
if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
|
||||
&& !(damageNeeded > currentDamage + additionalDamage)
|
||||
// The attacker will be killed
|
||||
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
|
||||
@@ -454,8 +449,7 @@ public class AiBlockController {
|
||||
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
|
||||
// or life is in danger
|
||||
&& CombatUtil.canBlock(attacker, blocker, combat)) {
|
||||
// this is needed for attackers that can't be blocked by
|
||||
// more than 1
|
||||
// this is needed for attackers that can't be blocked by more than 1
|
||||
currentAttackers.remove(attacker);
|
||||
combat.addBlocker(attacker, blocker);
|
||||
if (CombatUtil.canBlock(attacker, leader, combat)) {
|
||||
@@ -496,7 +490,7 @@ public class AiBlockController {
|
||||
final int addedValue3 = ComputerUtilCard.evaluateCreature(secondBlocker);
|
||||
final int netCombatDamage = attacker.getNetCombatDamage();
|
||||
|
||||
if ((damageNeeded > currentDamage || CombatUtil.needsBlockers(attacker) > blockGang.size())
|
||||
if ((damageNeeded > currentDamage || CombatUtil.getMinNumBlockersForAttacker(attacker, ai) > blockGang.size())
|
||||
&& !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
|
||||
// The attacker will be killed
|
||||
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
|
||||
@@ -510,8 +504,7 @@ public class AiBlockController {
|
||||
// or life is in danger
|
||||
&& CombatUtil.canBlock(attacker, secondBlocker, combat)
|
||||
&& CombatUtil.canBlock(attacker, thirdBlocker, combat)) {
|
||||
// this is needed for attackers that can't be blocked by
|
||||
// more than 1
|
||||
// this is needed for attackers that can't be blocked by more than 1
|
||||
currentAttackers.remove(attacker);
|
||||
combat.addBlocker(attacker, thirdBlocker);
|
||||
if (CombatUtil.canBlock(attacker, secondBlocker, combat)) {
|
||||
@@ -587,12 +580,10 @@ public class AiBlockController {
|
||||
* @param combat a {@link forge.game.combat.Combat} object.
|
||||
*/
|
||||
private void makeTradeBlocks(final Combat combat) {
|
||||
|
||||
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
||||
List<Card> killingBlockers;
|
||||
|
||||
for (final Card attacker : attackersLeft) {
|
||||
|
||||
if (attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||
|| attacker.hasKeyword(Keyword.MENACE)
|
||||
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
|
||||
@@ -628,7 +619,6 @@ public class AiBlockController {
|
||||
|
||||
// Chump Blocks (should only be made if life is in danger)
|
||||
private void makeChumpBlocks(final Combat combat) {
|
||||
|
||||
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
||||
|
||||
makeChumpBlocks(combat, currentAttackers);
|
||||
@@ -639,7 +629,6 @@ public class AiBlockController {
|
||||
}
|
||||
|
||||
private void makeChumpBlocks(final Combat combat, List<Card> attackers) {
|
||||
|
||||
if (attackers.isEmpty() || !ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
return;
|
||||
}
|
||||
@@ -694,11 +683,9 @@ public class AiBlockController {
|
||||
|
||||
// Block creatures with "can't be blocked except by two or more creatures"
|
||||
private void makeMultiChumpBlocks(final Combat combat) {
|
||||
|
||||
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
|
||||
|
||||
for (final Card attacker : currentAttackers) {
|
||||
|
||||
if (!attacker.hasStartOfKeyword("CantBeBlockedByAmount LT")
|
||||
&& !attacker.hasKeyword(Keyword.MENACE)
|
||||
&& !attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
|
||||
@@ -730,14 +717,12 @@ public class AiBlockController {
|
||||
|
||||
/** Reinforce blockers blocking attackers with trample (should only be made if life is in danger) */
|
||||
private void reinforceBlockersAgainstTrample(final Combat combat) {
|
||||
|
||||
List<Card> chumpBlockers;
|
||||
|
||||
List<Card> tramplingAttackers = CardLists.getKeyword(attackers, Keyword.TRAMPLE);
|
||||
tramplingAttackers = CardLists.filter(tramplingAttackers, Predicates.not(rampagesOrNeedsManyToBlock));
|
||||
|
||||
// TODO - should check here for a "rampage-like" trigger that replaced
|
||||
// the keyword:
|
||||
// TODO - should check here for a "rampage-like" trigger that replaced the keyword:
|
||||
// "Whenever CARDNAME becomes blocked, it gets +1/+1 until end of turn for each creature blocking it."
|
||||
|
||||
for (final Card attacker : tramplingAttackers) {
|
||||
@@ -764,7 +749,6 @@ public class AiBlockController {
|
||||
|
||||
/** Support blockers not destroying the attacker with more blockers to try to kill the attacker */
|
||||
private void reinforceBlockersToKill(final Combat combat) {
|
||||
|
||||
List<Card> safeBlockers;
|
||||
List<Card> blockers;
|
||||
List<Card> targetAttackers = CardLists.filter(blockedButUnkilled, Predicates.not(rampagesOrNeedsManyToBlock));
|
||||
@@ -1036,27 +1020,21 @@ public class AiBlockController {
|
||||
} else {
|
||||
lifeInDanger = false;
|
||||
}
|
||||
// if life is still in danger
|
||||
// Reinforce blockers blocking attackers with trample if life is
|
||||
// still
|
||||
// in danger
|
||||
// Reinforce blockers blocking attackers with trample if life is still in danger
|
||||
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
reinforceBlockersAgainstTrample(combat);
|
||||
} else {
|
||||
lifeInDanger = false;
|
||||
}
|
||||
// Support blockers not destroying the attacker with more blockers
|
||||
// to
|
||||
// try to kill the attacker
|
||||
// to try to kill the attacker
|
||||
if (!lifeInDanger) {
|
||||
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)) {
|
||||
clearBlockers(combat, possibleBlockers); // reset every block
|
||||
// assignment
|
||||
clearBlockers(combat, possibleBlockers); // reset every block assignment
|
||||
makeTradeBlocks(combat); // choose necessary trade blocks
|
||||
// if life is in danger
|
||||
makeGoodBlocks(combat);
|
||||
@@ -1066,8 +1044,7 @@ public class AiBlockController {
|
||||
} else {
|
||||
lifeInDanger = false;
|
||||
}
|
||||
// Reinforce blockers blocking attackers with trample if life is
|
||||
// still in danger
|
||||
// Reinforce blockers blocking attackers with trample if life is still in danger
|
||||
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
reinforceBlockersAgainstTrample(combat);
|
||||
} else {
|
||||
@@ -1077,11 +1054,9 @@ public class AiBlockController {
|
||||
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)) {
|
||||
clearBlockers(combat, possibleBlockers); // reset every block
|
||||
// assignment
|
||||
clearBlockers(combat, possibleBlockers); // reset every block assignment
|
||||
makeChumpBlocks(combat); // choose chump blocks
|
||||
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
makeTradeBlocks(combat); // choose necessary trade
|
||||
@@ -1090,15 +1065,13 @@ public class AiBlockController {
|
||||
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
makeGoodBlocks(combat);
|
||||
}
|
||||
// Reinforce blockers blocking attackers with trample if life is
|
||||
// still in danger
|
||||
// Reinforce blockers blocking attackers with trample if life is still in danger
|
||||
else {
|
||||
reinforceBlockersAgainstTrample(combat);
|
||||
}
|
||||
makeGangBlocks(combat);
|
||||
// Support blockers not destroying the attacker with more
|
||||
// blockers
|
||||
// to try to kill the attacker
|
||||
// blockers to try to kill the attacker
|
||||
reinforceBlockersToKill(combat);
|
||||
}
|
||||
}
|
||||
@@ -1108,7 +1081,7 @@ public class AiBlockController {
|
||||
chumpBlockers.addAll(CardLists.getKeyword(blockersLeft, "CARDNAME blocks each combat if able."));
|
||||
// if an attacker with lure attacks - all that can block
|
||||
for (final Card blocker : blockersLeft) {
|
||||
if (CombatUtil.mustBlockAnAttacker(blocker, combat)) {
|
||||
if (CombatUtil.mustBlockAnAttacker(blocker, combat, null)) {
|
||||
chumpBlockers.add(blocker);
|
||||
}
|
||||
}
|
||||
@@ -1118,7 +1091,7 @@ public class AiBlockController {
|
||||
blockers = getPossibleBlockers(combat, attacker, chumpBlockers, false);
|
||||
for (final Card blocker : blockers) {
|
||||
if (CombatUtil.canBlock(attacker, blocker, combat) && blockersLeft.contains(blocker)
|
||||
&& (CombatUtil.mustBlockAnAttacker(blocker, combat)
|
||||
&& (CombatUtil.mustBlockAnAttacker(blocker, combat, null)
|
||||
|| blocker.hasKeyword("CARDNAME blocks each turn if able.")
|
||||
|| blocker.hasKeyword("CARDNAME blocks each combat if able."))) {
|
||||
combat.addBlocker(attacker, blocker);
|
||||
@@ -1137,7 +1110,6 @@ 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)) {
|
||||
|
||||
@@ -32,7 +32,9 @@ import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.ability.ChangeZoneAi;
|
||||
import forge.ai.ability.ExploreAi;
|
||||
import forge.ai.ability.LearnAi;
|
||||
import forge.ai.simulation.SpellAbilityPicker;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.MagicColor;
|
||||
import forge.card.mana.ManaCost;
|
||||
import forge.deck.CardPool;
|
||||
@@ -45,6 +47,7 @@ import forge.game.Game;
|
||||
import forge.game.GameActionUtil;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.GlobalRuleChange;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.ability.SpellApiBased;
|
||||
@@ -77,6 +80,7 @@ import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.replacement.ReplaceMoved;
|
||||
import forge.game.replacement.ReplacementEffect;
|
||||
import forge.game.replacement.ReplacementLayer;
|
||||
import forge.game.replacement.ReplacementType;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.LandAbility;
|
||||
@@ -88,6 +92,7 @@ import forge.game.spellability.SpellAbilityCondition;
|
||||
import forge.game.spellability.SpellAbilityPredicates;
|
||||
import forge.game.spellability.SpellPermanent;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.trigger.WrappedAbility;
|
||||
@@ -305,6 +310,8 @@ public class AiController {
|
||||
}
|
||||
|
||||
exSA.setTrigger(tr);
|
||||
// need to set TriggeredObject
|
||||
exSA.setTriggeringObject(AbilityKey.Card, card);
|
||||
|
||||
// for trigger test, need to ignore the conditions
|
||||
SpellAbilityCondition cons = exSA.getConditions();
|
||||
@@ -430,17 +437,22 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
|
||||
// don't play the land if it has cycling and enough lands are available
|
||||
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
|
||||
|
||||
final CardCollectionView hand = player.getCardsIn(ZoneType.Hand);
|
||||
CardCollection lands = new CardCollection(battlefield);
|
||||
lands.addAll(hand);
|
||||
lands = CardLists.filter(lands, CardPredicates.Presets.LANDS);
|
||||
int maxCmcInHand = Aggregates.max(hand, CardPredicates.Accessors.fnGetCmc);
|
||||
for (final SpellAbility sa : spellAbilities) {
|
||||
if (sa.isCycling()) {
|
||||
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
|
||||
|
||||
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
|
||||
// don't play MDFC land if other side is spell and enough lands are available
|
||||
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// don't play the land if it has cycling and enough lands are available
|
||||
final FCollectionView<SpellAbility> spellAbilities = c.getSpellAbilities();
|
||||
for (final SpellAbility sa : spellAbilities) {
|
||||
if (sa.isCycling()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -501,20 +513,35 @@ public class AiController {
|
||||
|
||||
//try to skip lands that enter the battlefield tapped
|
||||
if (!nonLandsInHand.isEmpty()) {
|
||||
CardCollection nonTappeddLands = new CardCollection();
|
||||
CardCollection nonTappedLands = new CardCollection();
|
||||
for (Card land : landList) {
|
||||
// Is this the best way to check if a land ETB Tapped?
|
||||
if (land.hasSVar("ETBTappedSVar")) {
|
||||
// check replacement effects if land would enter tapped or not
|
||||
final Map<AbilityKey, Object> repParams = AbilityKey.mapFromAffected(land);
|
||||
repParams.put(AbilityKey.Origin, land.getZone().getZoneType());
|
||||
repParams.put(AbilityKey.Destination, ZoneType.Battlefield);
|
||||
repParams.put(AbilityKey.Source, land);
|
||||
|
||||
boolean foundTapped = false;
|
||||
for (ReplacementEffect re : player.getGame().getReplacementHandler().getReplacementList(ReplacementType.Moved, repParams, ReplacementLayer.Other)) {
|
||||
SpellAbility reSA = re.ensureAbility();
|
||||
if (reSA == null || !ApiType.Tap.equals(reSA.getApi())) {
|
||||
continue;
|
||||
}
|
||||
reSA.setActivatingPlayer(reSA.getHostCard().getController());
|
||||
if (reSA.metConditions()) {
|
||||
foundTapped = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (foundTapped) {
|
||||
continue;
|
||||
}
|
||||
// Glacial Fortress and friends
|
||||
if (land.hasSVar("ETBCheckSVar") && CardFactoryUtil.xCount(land, land.getSVar("ETBCheckSVar")) == 0) {
|
||||
continue;
|
||||
}
|
||||
nonTappeddLands.add(land);
|
||||
|
||||
nonTappedLands.add(land);
|
||||
}
|
||||
if (!nonTappeddLands.isEmpty()) {
|
||||
landList = nonTappeddLands;
|
||||
if (!nonTappedLands.isEmpty()) {
|
||||
landList = nonTappedLands;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -585,8 +612,7 @@ public class AiController {
|
||||
SpellAbility currentSA = sa;
|
||||
sa.setActivatingPlayer(player);
|
||||
// check everything necessary
|
||||
|
||||
|
||||
|
||||
AiPlayDecision opinion = canPlayAndPayFor(currentSA);
|
||||
//PhaseHandler ph = game.getPhaseHandler();
|
||||
// System.out.printf("Ai thinks '%s' of %s @ %s %s >>> \n", opinion, sa, Lang.getPossesive(ph.getPlayerTurn().getName()), ph.getPhase());
|
||||
@@ -826,8 +852,11 @@ public class AiController {
|
||||
}
|
||||
return canPlayFromEffectAI((SpellPermanent)sa, false, true);
|
||||
}
|
||||
if (sa.usesTargeting() && !sa.isTargetNumberValid()) {
|
||||
if (!sa.getTargetRestrictions().hasCandidates(sa, true)) {
|
||||
if (sa.usesTargeting()) {
|
||||
if (!sa.isTargetNumberValid() && !sa.getTargetRestrictions().hasCandidates(sa, true)) {
|
||||
return AiPlayDecision.TargetingFailed;
|
||||
}
|
||||
if (!StaticAbilityMustTarget.meetsMustTargetRestriction(sa)) {
|
||||
return AiPlayDecision.TargetingFailed;
|
||||
}
|
||||
}
|
||||
@@ -1110,12 +1139,18 @@ public class AiController {
|
||||
final CardCollection discardList = new CardCollection();
|
||||
int count = 0;
|
||||
if (sa != null) {
|
||||
String logic = sa.getParamOrDefault("AILogic", "");
|
||||
sourceCard = sa.getHostCard();
|
||||
if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) {
|
||||
if ("Always".equals(logic) && !validCards.isEmpty()) {
|
||||
min = 1;
|
||||
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
|
||||
} else if (logic.startsWith("UnlessAtLife.")) {
|
||||
int threshold = AbilityUtils.calculateAmount(sourceCard, logic.substring(logic.indexOf(".") + 1), sa);
|
||||
if (player.getLife() <= threshold) {
|
||||
min = 1;
|
||||
}
|
||||
} else if ("VolrathsShapeshifter".equals(logic)) {
|
||||
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
|
||||
} else if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
|
||||
} else if ("DiscardCMCX".equals(logic)) {
|
||||
final int cmc = sa.getXManaCostPaid();
|
||||
CardCollection discards = CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.hasCMC(cmc));
|
||||
if (discards.isEmpty()) {
|
||||
@@ -1290,15 +1325,6 @@ public class AiController {
|
||||
}
|
||||
|
||||
public boolean confirmStaticApplication(Card hostCard, GameEntity affected, String logic, String message) {
|
||||
if (logic.equalsIgnoreCase("ProtectFriendly")) {
|
||||
final Player controller = hostCard.getController();
|
||||
if (affected instanceof Player) {
|
||||
return !((Player) affected).isOpponentOf(controller);
|
||||
}
|
||||
if (affected instanceof Card) {
|
||||
return !((Card) affected).getController().isOpponentOf(controller);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1328,7 +1354,7 @@ public class AiController {
|
||||
|
||||
public AiPlayDecision canPlayFromEffectAI(Spell spell, boolean mandatory, boolean withoutPayingManaCost) {
|
||||
int damage = ComputerUtil.getDamageForPlaying(player, spell);
|
||||
if (damage >= player.getLife() && !player.cantLoseForZeroOrLessLife() && player.canLoseLife()) {
|
||||
if (!mandatory && damage >= player.getLife() && !player.cantLoseForZeroOrLessLife() && player.canLoseLife()) {
|
||||
return AiPlayDecision.CurseEffects;
|
||||
}
|
||||
|
||||
@@ -1397,8 +1423,6 @@ public class AiController {
|
||||
private List<SpellAbility> singleSpellAbilityList(SpellAbility sa) {
|
||||
if (sa == null) { return null; }
|
||||
|
||||
// System.out.println("Chosen to play: " + sa);
|
||||
|
||||
final List<SpellAbility> abilities = Lists.newArrayList();
|
||||
abilities.add(sa);
|
||||
return abilities;
|
||||
@@ -1443,6 +1467,13 @@ public class AiController {
|
||||
if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) {
|
||||
final List<SpellAbility> abilities = Lists.newArrayList();
|
||||
|
||||
// TODO extend this logic to evaluate MDFC with both sides land
|
||||
// this can only happen if its a MDFC land
|
||||
if (!land.isLand()) {
|
||||
land.setState(CardStateName.Modal, true);
|
||||
land.setBackSide(true);
|
||||
}
|
||||
|
||||
LandAbility la = new LandAbility(land, player, null);
|
||||
la.setCardState(land.getCurrentState());
|
||||
if (la.canPlay()) {
|
||||
@@ -1693,10 +1724,8 @@ public class AiController {
|
||||
for (int i = 0; i < numToExile; i++) {
|
||||
Card chosen = null;
|
||||
for (final Card c : grave) { // Exile noncreatures first in
|
||||
// case we can revive. Might
|
||||
// wanna do some additional
|
||||
// checking here for Flashback
|
||||
// and the like.
|
||||
// case we can revive. Might wanna do some additional
|
||||
// checking here for Flashback and the like.
|
||||
if (!c.isCreature()) {
|
||||
chosen = c;
|
||||
break;
|
||||
@@ -1735,12 +1764,21 @@ public class AiController {
|
||||
* @param sa the sa
|
||||
* @return true, if successful
|
||||
*/
|
||||
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa) {
|
||||
public final boolean aiShouldRun(final ReplacementEffect effect, final SpellAbility sa, GameEntity affected) {
|
||||
Card hostCard = effect.getHostCard();
|
||||
if (hostCard.hasAlternateState()) {
|
||||
hostCard = game.getCardState(hostCard);
|
||||
}
|
||||
|
||||
if (effect.hasParam("AILogic") && effect.getParam("AILogic").equalsIgnoreCase("ProtectFriendly")) {
|
||||
final Player controller = hostCard.getController();
|
||||
if (affected instanceof Player) {
|
||||
return !((Player) affected).isOpponentOf(controller);
|
||||
}
|
||||
if (affected instanceof Card) {
|
||||
return !((Card) affected).getController().isOpponentOf(controller);
|
||||
}
|
||||
}
|
||||
if (effect.hasParam("AICheckSVar")) {
|
||||
System.out.println("aiShouldRun?" + sa);
|
||||
final String svarToCheck = effect.getParam("AICheckSVar");
|
||||
@@ -1755,7 +1793,7 @@ public class AiController {
|
||||
compareTo = Integer.parseInt(strCmpTo);
|
||||
} catch (final Exception ignored) {
|
||||
if (sa == null) {
|
||||
compareTo = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(strCmpTo));
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), effect);
|
||||
} else {
|
||||
compareTo = AbilityUtils.calculateAmount(hostCard, hostCard.getSVar(strCmpTo), sa);
|
||||
}
|
||||
@@ -1765,7 +1803,7 @@ public class AiController {
|
||||
int left = 0;
|
||||
|
||||
if (sa == null) {
|
||||
left = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(svarToCheck));
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, effect);
|
||||
} else {
|
||||
left = AbilityUtils.calculateAmount(hostCard, svarToCheck, sa);
|
||||
}
|
||||
@@ -1855,11 +1893,13 @@ public class AiController {
|
||||
} else if ("LowestLoseLife".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
|
||||
} else if ("HighestLoseLife".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1;
|
||||
return Math.min(player.getLife() -1,MyRandom.getRandom().nextInt(Math.max(player.getLife() / 3, player.getWeakestOpponent().getLife())) + 1);
|
||||
} else if ("HighestGetCounter".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(3);
|
||||
} else if (source.hasSVar("EnergyToPay")) {
|
||||
return AbilityUtils.calculateAmount(source, source.getSVar("EnergyToPay"), sa);
|
||||
} else if ("Vermin".equals(logic)) {
|
||||
return MyRandom.getRandom().nextInt(Math.max(player.getLife() - 5, 0));
|
||||
}
|
||||
return max;
|
||||
}
|
||||
@@ -1954,7 +1994,6 @@ public class AiController {
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
// this is where the computer cheats
|
||||
// changes AllZone.getComputerPlayer().getZone(Zone.Library)
|
||||
|
||||
@@ -2089,8 +2128,11 @@ public class AiController {
|
||||
if (useSimulation) {
|
||||
return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||
}
|
||||
|
||||
if (sa.getApi() == ApiType.Explore) {
|
||||
return ExploreAi.shouldPutInGraveyard(fetchList, decider);
|
||||
} else if (sa.getApi() == ApiType.Learn) {
|
||||
return LearnAi.chooseCardToLearn(fetchList, decider, sa);
|
||||
} else {
|
||||
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||
}
|
||||
@@ -2231,10 +2273,8 @@ public class AiController {
|
||||
}
|
||||
}
|
||||
|
||||
// AI logic for choosing which replacement effect to apply
|
||||
// happens here.
|
||||
// AI logic for choosing which replacement effect to apply happens here.
|
||||
return Iterables.getFirst(list, null);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,11 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import forge.game.cost.*;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.card.CardType;
|
||||
@@ -22,43 +24,12 @@ import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.card.CounterType;
|
||||
import forge.game.cost.CostAddMana;
|
||||
import forge.game.cost.CostChooseCreatureType;
|
||||
import forge.game.cost.CostDamage;
|
||||
import forge.game.cost.CostDecisionMakerBase;
|
||||
import forge.game.cost.CostDiscard;
|
||||
import forge.game.cost.CostDraw;
|
||||
import forge.game.cost.CostExert;
|
||||
import forge.game.cost.CostExile;
|
||||
import forge.game.cost.CostExileFromStack;
|
||||
import forge.game.cost.CostExiledMoveToGrave;
|
||||
import forge.game.cost.CostFlipCoin;
|
||||
import forge.game.cost.CostGainControl;
|
||||
import forge.game.cost.CostGainLife;
|
||||
import forge.game.cost.CostMill;
|
||||
import forge.game.cost.CostPartMana;
|
||||
import forge.game.cost.CostPayEnergy;
|
||||
import forge.game.cost.CostPayLife;
|
||||
import forge.game.cost.CostPutCardToLib;
|
||||
import forge.game.cost.CostPutCounter;
|
||||
import forge.game.cost.CostRemoveAnyCounter;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.cost.CostReturn;
|
||||
import forge.game.cost.CostReveal;
|
||||
import forge.game.cost.CostSacrifice;
|
||||
import forge.game.cost.CostTap;
|
||||
import forge.game.cost.CostTapType;
|
||||
import forge.game.cost.CostUnattach;
|
||||
import forge.game.cost.CostUntap;
|
||||
import forge.game.cost.CostUntapType;
|
||||
import forge.game.cost.PaymentDecision;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityStackInstance;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.Localizer;
|
||||
import forge.util.TextUtil;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
@@ -92,15 +63,9 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostChooseCreatureType cost) {
|
||||
Integer amount = cost.convertAmount();
|
||||
Iterable<String> choices = player.getController().chooseSomeType(
|
||||
Localizer.getInstance().getMessage("lblCreature"), ability, amount, amount, Lists.newArrayList(CardType.getAllCreatureTypes()));
|
||||
|
||||
if (choices == null || Iterables.isEmpty(choices)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return PaymentDecision.types(choices);
|
||||
String choice = player.getController().chooseSomeType("Creature", ability, CardType.getAllCreatureTypes(),
|
||||
Lists.newArrayList());
|
||||
return PaymentDecision.type(choice);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -583,6 +548,11 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
return PaymentDecision.card(aic.getCardsToDiscard(c, type.split(";"), ability));
|
||||
}
|
||||
|
||||
@Override
|
||||
public PaymentDecision visit(CostRevealChosenPlayer cost) {
|
||||
return PaymentDecision.number(1);
|
||||
}
|
||||
|
||||
protected int removeCounter(GameEntityCounterTable table, List<Card> prefs, CounterEnumType cType, int stillToRemove) {
|
||||
int removed = 0;
|
||||
if (!prefs.isEmpty() && stillToRemove > 0) {
|
||||
@@ -596,7 +566,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
|
||||
if (thisRemove > 0) {
|
||||
removed += thisRemove;
|
||||
table.put(prefCard, CounterType.get(cType), thisRemove);
|
||||
table.put(null, prefCard, CounterType.get(cType), thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,7 +577,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
public PaymentDecision visit(CostRemoveAnyCounter cost) {
|
||||
final String amount = cost.getAmount();
|
||||
final int c = AbilityUtils.calculateAmount(source, amount, ability);
|
||||
final Card originalHost = ability.getOriginalOrHost();
|
||||
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
|
||||
|
||||
if (c <= 0) {
|
||||
return null;
|
||||
@@ -662,7 +632,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(card.getCounters(ctype), c - toRemove);
|
||||
if (thisRemove > 0) {
|
||||
toRemove += thisRemove;
|
||||
table.put(card, ctype, thisRemove);
|
||||
table.put(null, card, ctype, thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -690,7 +660,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int over = Math.min(e.getValue(), c - toRemove);
|
||||
if (over > 0) {
|
||||
toRemove += over;
|
||||
table.put(crd, e.getKey(), over);
|
||||
table.put(null, crd, e.getKey(), over);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -720,7 +690,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int over = Math.min(e.getValue(), c - toRemove);
|
||||
if (over > 0) {
|
||||
toRemove += over;
|
||||
table.put(crd, e.getKey(), over);
|
||||
table.put(null, crd, e.getKey(), over);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -760,7 +730,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
|
||||
if (over > 0) {
|
||||
toRemove += over;
|
||||
table.put(crd, CounterType.get(CounterEnumType.QUEST), over);
|
||||
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -784,7 +754,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(card.getCounters(cost.counter), c - toRemove);
|
||||
if (thisRemove > 0) {
|
||||
toRemove += thisRemove;
|
||||
table.put(card, cost.counter, thisRemove);
|
||||
table.put(null, card, cost.counter, thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -798,7 +768,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
|
||||
int thisRemove = Math.min(e.getValue(), c - toRemove);
|
||||
if (thisRemove > 0) {
|
||||
toRemove += thisRemove;
|
||||
table.put(card, e.getKey(), thisRemove);
|
||||
table.put(null, card, e.getKey(), thisRemove);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,6 @@ public class ComputerUtil {
|
||||
final Card source = sa.getHostCard();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
|
||||
|
||||
// Play higher costing spells first?
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
@@ -213,7 +212,8 @@ public class ComputerUtil {
|
||||
if (unless != null && !unless.endsWith(">")) {
|
||||
final int amount = AbilityUtils.calculateAmount(source, unless, sa);
|
||||
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtil.getOpponentFor(ai), true).size();
|
||||
// this is enough as long as the AI is only smart enough to target top of stack
|
||||
final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtilAbility.getTopSpellAbilityOnStack(ai.getGame(), sa).getActivatingPlayer(), true).size();
|
||||
|
||||
// If the Unless isn't enough, this should be less likely to be used
|
||||
if (amount > usableManaSources) {
|
||||
@@ -280,7 +280,7 @@ public class ComputerUtil {
|
||||
SpellAbility newSA = sa.copyWithNoManaCost();
|
||||
newSA.setActivatingPlayer(ai);
|
||||
|
||||
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA)) {
|
||||
if (!CostPayment.canPayAdditionalCosts(newSA.getPayCosts(), newSA) || !ComputerUtilMana.canPayManaCost(newSA, ai, 0)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1068,9 +1068,6 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ComputerUtil.getOpponentFor(ai))) {
|
||||
return true;
|
||||
}
|
||||
if (card.isCreature()) {
|
||||
if (buffedcard.hasKeyword(Keyword.SOULBOND) && !buffedcard.isPaired()) {
|
||||
return true;
|
||||
@@ -1093,8 +1090,8 @@ public class ComputerUtil {
|
||||
|
||||
} // BuffedBy
|
||||
|
||||
// get all cards the human controls with AntiBuffedBy
|
||||
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
// there's a good chance AI will attack weak target
|
||||
final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
for (Card buffedcard : antibuffed) {
|
||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||
@@ -1128,7 +1125,7 @@ public class ComputerUtil {
|
||||
creatures2.add(creatures.get(i));
|
||||
}
|
||||
}
|
||||
if (((creatures2.size() + CardUtil.getThisTurnCast("Creature.YouCtrl", vengevines.get(0)).size()) > 1)
|
||||
if (((creatures2.size() + CardUtil.getThisTurnCast("Creature.YouCtrl", vengevines.get(0), null).size()) > 1)
|
||||
&& card.isCreature() && card.getManaCost().getCMC() <= 3) {
|
||||
return true;
|
||||
}
|
||||
@@ -1142,27 +1139,16 @@ public class ComputerUtil {
|
||||
* @return true if it's OK to cast this Card for less than the max targets
|
||||
*/
|
||||
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
|
||||
boolean ret = true;
|
||||
if (source.getManaCost().countX() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by mana available.
|
||||
return ret;
|
||||
} else {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
Combat combat = new Combat(ComputerUtil.getOpponentFor(ai));
|
||||
CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
|
||||
for (Card att : attackers) {
|
||||
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
|
||||
combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController()));
|
||||
}
|
||||
}
|
||||
AiBlockController aiBlock = new AiBlockController(ai);
|
||||
aiBlock.assignBlockersForCombat(combat);
|
||||
if (!ComputerUtilCombat.lifeInDanger(ai, combat)) {
|
||||
// Otherwise, return false. Do not play now.
|
||||
ret = false;
|
||||
}
|
||||
if (source.getXManaCostPaid() > 0) {
|
||||
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
|
||||
return true;
|
||||
}
|
||||
return ret;
|
||||
if (aiLifeInDanger(ai, false, 0)) {
|
||||
// Otherwise, if life is possibly in danger, then this is fine.
|
||||
return true;
|
||||
}
|
||||
// do not play now.
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1266,8 +1252,8 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
// get all cards the human controls with AntiBuffedBy
|
||||
final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
|
||||
// there's a good chance AI will attack weak target
|
||||
final CardCollectionView antibuffed = ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield);
|
||||
for (Card buffedcard : antibuffed) {
|
||||
if (buffedcard.hasSVar("AntiBuffedBy")) {
|
||||
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
|
||||
@@ -1463,7 +1449,7 @@ public class ComputerUtil {
|
||||
return false;
|
||||
}
|
||||
|
||||
public static int possibleNonCombatDamage(Player ai) {
|
||||
public static int possibleNonCombatDamage(Player ai, Player enemy) {
|
||||
int damage = 0;
|
||||
final CardCollection all = new CardCollection(ai.getCardsIn(ZoneType.Battlefield));
|
||||
all.addAll(ai.getCardsActivableInExternalZones(true));
|
||||
@@ -1483,7 +1469,6 @@ public class ComputerUtil {
|
||||
if (tgt == null) {
|
||||
continue;
|
||||
}
|
||||
final Player enemy = ComputerUtil.getOpponentFor(ai);
|
||||
if (!sa.canTarget(enemy)) {
|
||||
continue;
|
||||
}
|
||||
@@ -2256,42 +2241,40 @@ public class ComputerUtil {
|
||||
return getCardsToDiscardFromOpponent(aiChooser, p, sa, validCards, min, max);
|
||||
}
|
||||
|
||||
public static List<String> chooseSomeType(Player ai, String kindOfType, String logic, int min, int max, Collection<String> validTypes) {
|
||||
public static String chooseSomeType(Player ai, String kindOfType, String logic, Collection<String> validTypes, List<String> invalidTypes) {
|
||||
if (invalidTypes == null) {
|
||||
invalidTypes = ImmutableList.of();
|
||||
}
|
||||
if (validTypes == null) {
|
||||
validTypes = ImmutableList.of();
|
||||
}
|
||||
|
||||
final Game game = ai.getGame();
|
||||
List<String> chosenList = Lists.newArrayList();
|
||||
String chosen = "";
|
||||
if (kindOfType.equals("Card")) {
|
||||
String chosen = "";
|
||||
|
||||
// TODO
|
||||
// computer will need to choose a type
|
||||
// based on whether it needs a creature or land,
|
||||
// otherwise, lib search for most common type left
|
||||
// then, reveal chosenType to Human
|
||||
// computer will need to choose a type based on whether it needs a creature or land,
|
||||
// otherwise, lib search for most common type left then, reveal chosenType to Human
|
||||
if (game.getPhaseHandler().is(PhaseType.UNTAP) && logic == null) { // Storage Matrix
|
||||
double amount = 0;
|
||||
for (String type : validTypes) {
|
||||
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType(type), Presets.TAPPED);
|
||||
double i = type.equals("Creature") ? list.size() * 1.5 : list.size();
|
||||
if (i > amount) {
|
||||
amount = i;
|
||||
chosen = type;
|
||||
for (String type : CardType.getAllCardTypes()) {
|
||||
if (!invalidTypes.contains(type)) {
|
||||
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isType(type), Presets.TAPPED);
|
||||
double i = type.equals("Creature") ? list.size() * 1.5 : list.size();
|
||||
if (i > amount) {
|
||||
amount = i;
|
||||
chosen = type;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (logic == "MostProminentInComputerDeck") {
|
||||
chosen = ComputerUtilCard.getMostProminentType(ai.getAllCards(), validTypes);
|
||||
}
|
||||
if (StringUtils.isEmpty(chosen)) {
|
||||
chosen = validTypes.isEmpty() ? "Creature" : Aggregates.random(validTypes);
|
||||
}
|
||||
chosenList.add(chosen);
|
||||
} else if (kindOfType.equals("Creature")) {
|
||||
String chosen = "";
|
||||
if (logic != null) {
|
||||
List <String> valid = Lists.newArrayList(validTypes);
|
||||
List <String> valid = Lists.newArrayList(CardType.getAllCreatureTypes());
|
||||
valid.removeAll(invalidTypes);
|
||||
|
||||
if (logic.equals("MostProminentOnBattlefield")) {
|
||||
chosen = ComputerUtilCard.getMostProminentType(game.getCardsIn(ZoneType.Battlefield), valid);
|
||||
@@ -2302,7 +2285,7 @@ public class ComputerUtil {
|
||||
else if (logic.equals("MostProminentOppControls")) {
|
||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
||||
chosen = ComputerUtilCard.getMostProminentType(list, valid);
|
||||
if (!CardType.isACreatureType(chosen) || !validTypes.contains(chosen)) {
|
||||
if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
|
||||
list = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
|
||||
chosen = ComputerUtilCard.getMostProminentType(list, valid);
|
||||
}
|
||||
@@ -2314,17 +2297,16 @@ public class ComputerUtil {
|
||||
chosen = ComputerUtilCard.getMostProminentType(ai.getCardsIn(ZoneType.Graveyard), valid);
|
||||
}
|
||||
}
|
||||
if (!CardType.isACreatureType(chosen) || !validTypes.contains(chosen)) {
|
||||
chosen = Iterables.getFirst(validTypes, null);
|
||||
if (!CardType.isACreatureType(chosen) || invalidTypes.contains(chosen)) {
|
||||
chosen = "Sliver";
|
||||
}
|
||||
|
||||
chosenList.add(chosen);
|
||||
} else if (kindOfType.equals("Basic Land")) {
|
||||
String chosen = "";
|
||||
if (logic != null) {
|
||||
if (logic.equals("MostProminentOppControls")) {
|
||||
CardCollection list = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
||||
List<String> valid = Lists.newArrayList(validTypes);
|
||||
List<String> valid = Lists.newArrayList(CardType.getBasicTypes());
|
||||
valid.removeAll(invalidTypes);
|
||||
|
||||
chosen = ComputerUtilCard.getMostProminentType(list, valid);
|
||||
} else if (logic.equals("MostNeededType")) {
|
||||
@@ -2347,9 +2329,9 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
else if (logic.equals("ChosenLandwalk")) {
|
||||
for (Card c : ai.getWeakestOpponent().getLandsInPlay()) {
|
||||
for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
|
||||
for (String t : c.getType()) {
|
||||
if (validTypes.contains(t) && CardType.isABasicLandType(t)) {
|
||||
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
|
||||
chosen = t;
|
||||
break;
|
||||
}
|
||||
@@ -2358,28 +2340,16 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
|
||||
if (!CardType.isABasicLandType(chosen) || !validTypes.contains(chosen)) {
|
||||
chosen = Iterables.getFirst(validTypes, null);
|
||||
}
|
||||
chosenList.add(chosen);
|
||||
// the only one with choosing two types is Illusionary Terrain
|
||||
// and it needs other AI logic
|
||||
while (chosenList.size() < min) {
|
||||
validTypes.remove(chosen);
|
||||
chosen = Iterables.getFirst(validTypes, null);
|
||||
if (chosen == null) {
|
||||
return Lists.newArrayList();
|
||||
}
|
||||
chosenList.add(chosen);
|
||||
if (!CardType.isABasicLandType(chosen) || invalidTypes.contains(chosen)) {
|
||||
chosen = "Island";
|
||||
}
|
||||
}
|
||||
else if (kindOfType.equals("Land")) {
|
||||
String chosen = "";
|
||||
if (logic != null) {
|
||||
if (logic.equals("ChosenLandwalk")) {
|
||||
for (Card c : ai.getWeakestOpponent().getLandsInPlay()) {
|
||||
for (Card c : AiAttackController.choosePreferredDefenderPlayer(ai).getLandsInPlay()) {
|
||||
for (String t : c.getType().getLandTypes()) {
|
||||
if (validTypes.contains(t)) {
|
||||
if (!invalidTypes.contains(t)) {
|
||||
chosen = t;
|
||||
break;
|
||||
}
|
||||
@@ -2388,11 +2358,10 @@ public class ComputerUtil {
|
||||
}
|
||||
}
|
||||
if (StringUtils.isEmpty(chosen)) {
|
||||
chosen = Iterables.getFirst(validTypes, null);
|
||||
chosen = "Island";
|
||||
}
|
||||
chosenList.add(chosen);
|
||||
}
|
||||
return chosenList;
|
||||
return chosen;
|
||||
}
|
||||
|
||||
public static Object vote(Player ai, List<Object> options, SpellAbility sa, Multimap<Object, Player> votes, Player forPlayer) {
|
||||
@@ -2413,23 +2382,26 @@ public class ComputerUtil {
|
||||
case "Torture":
|
||||
return "Torture";
|
||||
case "GraceOrCondemnation":
|
||||
return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace"
|
||||
: "Condemnation";
|
||||
List<ZoneType> graceZones = new ArrayList<ZoneType>();
|
||||
graceZones.add(ZoneType.Battlefield);
|
||||
graceZones.add(ZoneType.Graveyard);
|
||||
CardCollection graceCreatures = CardLists.getType(sa.getHostCard().getGame().getCardsIn(graceZones), "Creature");
|
||||
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
|
||||
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
|
||||
return aiGrace > humanGrace ? "Grace" : "Condemnation";
|
||||
case "CarnageOrHomage":
|
||||
CardCollection cardsInPlay = CardLists
|
||||
.getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land");
|
||||
CardCollection cardsInPlay = CardLists.getNotType(sa.getHostCard().getGame().getCardsIn(ZoneType.Battlefield), "Land");
|
||||
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
|
||||
CardCollection computerlist = CardLists.filterControlledBy(cardsInPlay, ai);
|
||||
return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard
|
||||
.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
|
||||
CardCollection computerlist = ai.getCreaturesInPlay();
|
||||
return (ComputerUtilCard.evaluatePermanentList(computerlist) + 3) < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
|
||||
case "Judgment":
|
||||
if (votes.isEmpty()) {
|
||||
CardCollection list = new CardCollection();
|
||||
for (Object o : options) {
|
||||
if (o instanceof Card) {
|
||||
list.add((Card) o);
|
||||
}
|
||||
}
|
||||
}
|
||||
return ComputerUtilCard.getBestAI(list);
|
||||
} else {
|
||||
return Iterables.getFirst(votes.keySet(), null);
|
||||
@@ -2460,8 +2432,7 @@ public class ComputerUtil {
|
||||
if (!source.canReceiveCounters(p1p1Type)) {
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
// if source is not on the battlefield anymore, choose +1/+1
|
||||
// ones
|
||||
// if source is not on the battlefield anymore, choose +1/+1 ones
|
||||
if (!game.getCardState(source).isInZone(ZoneType.Battlefield)) {
|
||||
return opponent ? "Feather" : "Quill";
|
||||
}
|
||||
@@ -2503,8 +2474,7 @@ public class ComputerUtil {
|
||||
return opponent ? "Numbers" : "Strength";
|
||||
}
|
||||
|
||||
// TODO check for ETB to +1/+1 counters
|
||||
// or over another trigger like lifegain
|
||||
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
|
||||
|
||||
int tokenScore = ComputerUtilCard.evaluateCreature(token);
|
||||
|
||||
@@ -2582,8 +2552,7 @@ public class ComputerUtil {
|
||||
return "Taxes";
|
||||
} else {
|
||||
// ai is first voter or ally of controller
|
||||
// both are not affected, but if opponents controll creatures,
|
||||
// sacrifice is worse
|
||||
// both are not affected, but if opponents control creatures, sacrifice is worse
|
||||
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
|
||||
}
|
||||
default:
|
||||
@@ -2667,7 +2636,7 @@ public class ComputerUtil {
|
||||
continue;
|
||||
}
|
||||
if (trigger.hasParam("ValidCard")) {
|
||||
if (!card.isValid(trigger.getParam("ValidCard"), source.getController(), source, sa)) {
|
||||
if (!card.isValid(trigger.getParam("ValidCard").split(","), source.getController(), source, sa)) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -2880,7 +2849,6 @@ public class ComputerUtil {
|
||||
}
|
||||
|
||||
public static boolean lifegainNegative(final Player player, final Card source, final int n) {
|
||||
|
||||
if (!player.canGainLife()) {
|
||||
return false;
|
||||
}
|
||||
@@ -2948,23 +2916,6 @@ public class ComputerUtil {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@Deprecated
|
||||
public static final Player getOpponentFor(final Player player) {
|
||||
// This method is deprecated and currently functions as a synonym for player.getWeakestOpponent
|
||||
// until it can be replaced everywhere in the code.
|
||||
|
||||
// Consider replacing calls to this method either with a multiplayer-friendly determination of
|
||||
// opponent that contextually makes the most sense, or with a direct call to player.getWeakestOpponent
|
||||
// where that is applicable and makes sense from the point of view of multiplayer AI logic.
|
||||
Player opponent = player.getWeakestOpponent();
|
||||
if (opponent != null) {
|
||||
return opponent;
|
||||
}
|
||||
|
||||
throw new IllegalStateException("No opponents left ingame for " + player);
|
||||
}
|
||||
|
||||
public static int countUsefulCreatures(Player p) {
|
||||
CardCollection creats = p.getCreaturesInPlay();
|
||||
int count = 0;
|
||||
@@ -3047,31 +2998,32 @@ public class ComputerUtil {
|
||||
// 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;
|
||||
for (Player opponent: ai.getOpponents()) {
|
||||
// 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);
|
||||
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.
|
||||
// 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;
|
||||
if ((serious) && (ComputerUtilCombat.lifeInSeriousDanger(ai, combat, payment))) {
|
||||
return true;
|
||||
}
|
||||
if ((!serious) && (ComputerUtilCombat.lifeInDanger(ai, combat, payment))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
||||
|
||||
@@ -171,8 +171,12 @@ public class ComputerUtilAbility {
|
||||
return sa;
|
||||
}
|
||||
|
||||
public static Card getAbilitySource(SpellAbility sa) {
|
||||
return sa.getOriginalHost() != null ? sa.getOriginalHost() : sa.getHostCard();
|
||||
}
|
||||
|
||||
public static String getAbilitySourceName(SpellAbility sa) {
|
||||
final Card c = sa.getOriginalOrHost();
|
||||
final Card c = getAbilitySource(sa);
|
||||
return c != null ? c.getName() : "";
|
||||
}
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ import forge.game.combat.Combat;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPayEnergy;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.keyword.KeywordCollection;
|
||||
import forge.game.keyword.KeywordInterface;
|
||||
@@ -76,7 +77,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
});
|
||||
}
|
||||
return ComputerUtilCard.getMostExpensivePermanentAI(all);
|
||||
return getMostExpensivePermanentAI(all);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -137,6 +138,56 @@ public class ComputerUtilCard {
|
||||
return Aggregates.itemWithMin(all, CardPredicates.Accessors.fnGetCmc);
|
||||
}
|
||||
|
||||
public static 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(CounterEnumType.LOYALTY);
|
||||
int pwScore = curLoyalty * 10;
|
||||
|
||||
for (SpellAbility sa : pw.getSpellAbilities()) {
|
||||
if (sa.hasParam("Ultimate")) {
|
||||
Integer 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 != null && loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
|
||||
// Will ultimate soon
|
||||
pwScore += 10000;
|
||||
}
|
||||
|
||||
if (pwScore > bestScore) {
|
||||
bestScore = pwScore;
|
||||
bestTgt = pw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestTgt;
|
||||
}
|
||||
|
||||
public static Card getWorstPlaneswalkerToDamage(final List<Card> pws) {
|
||||
Card bestTgt = null;
|
||||
|
||||
int bestScore = Integer.MAX_VALUE;
|
||||
for (Card pw : pws) {
|
||||
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
|
||||
|
||||
if (curLoyalty < bestScore) {
|
||||
bestScore = curLoyalty;
|
||||
bestTgt = pw;
|
||||
}
|
||||
}
|
||||
|
||||
return bestTgt;
|
||||
}
|
||||
|
||||
// The AI doesn't really pick the best enchantment, just the most expensive.
|
||||
/**
|
||||
* <p>
|
||||
@@ -265,16 +316,14 @@ public class ComputerUtilCard {
|
||||
* @return a {@link forge.game.card.Card} object.
|
||||
*/
|
||||
public static Card getBestAI(final Iterable<Card> list) {
|
||||
// Get Best will filter by appropriate getBest list if ALL of the list
|
||||
// is of that type
|
||||
// Get Best will filter by appropriate getBest list if ALL of the list is of that type
|
||||
if (Iterables.all(list, CardPredicates.Presets.CREATURES)) {
|
||||
return ComputerUtilCard.getBestCreatureAI(list);
|
||||
}
|
||||
if (Iterables.all(list, CardPredicates.Presets.LANDS)) {
|
||||
return getBestLandAI(list);
|
||||
}
|
||||
// TODO - Once we get an EvaluatePermanent this should call
|
||||
// getBestPermanent()
|
||||
// TODO - Once we get an EvaluatePermanent this should call getBestPermanent()
|
||||
return ComputerUtilCard.getMostExpensivePermanentAI(list);
|
||||
}
|
||||
|
||||
@@ -383,7 +432,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
|
||||
if (biasLand && Iterables.any(list, CardPredicates.Presets.LANDS)) {
|
||||
return ComputerUtilCard.getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
|
||||
return getWorstLand(CardLists.filter(list, CardPredicates.Presets.LANDS));
|
||||
}
|
||||
|
||||
final boolean hasCreatures = Iterables.any(list, CardPredicates.Presets.CREATURES);
|
||||
@@ -393,7 +442,7 @@ public class ComputerUtilCard {
|
||||
|
||||
List<Card> lands = CardLists.filter(list, CardPredicates.Presets.LANDS);
|
||||
if (lands.size() > 6) {
|
||||
return ComputerUtilCard.getWorstLand(lands);
|
||||
return getWorstLand(lands);
|
||||
}
|
||||
|
||||
if (hasEnchantmants || hasArtifacts) {
|
||||
@@ -410,8 +459,7 @@ public class ComputerUtilCard {
|
||||
return getWorstCreatureAI(CardLists.filter(list, CardPredicates.Presets.CREATURES));
|
||||
}
|
||||
|
||||
// Planeswalkers fall through to here, lands will fall through if there
|
||||
// aren't very many
|
||||
// Planeswalkers fall through to here, lands will fall through if there aren't very many
|
||||
return getCheapestPermanentAI(list, null, false);
|
||||
}
|
||||
|
||||
@@ -444,7 +492,7 @@ public class ComputerUtilCard {
|
||||
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
|
||||
@Override
|
||||
public int compare(final Card a, final Card b) {
|
||||
return ComputerUtilCard.evaluateCreature(b) - ComputerUtilCard.evaluateCreature(a);
|
||||
return evaluateCreature(b) - evaluateCreature(a);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -550,7 +598,7 @@ public class ComputerUtilCard {
|
||||
*/
|
||||
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
|
||||
AiBlockController aiBlk = new AiBlockController(ai);
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
Combat combat = new Combat(opp);
|
||||
//Use actual attackers if available, else consider all possible attackers
|
||||
Combat currentCombat = ai.getGame().getCombat();
|
||||
@@ -882,9 +930,9 @@ public class ComputerUtilCard {
|
||||
public static final Predicate<Deck> AI_KNOWS_HOW_TO_PLAY_ALL_CARDS = new Predicate<Deck>() {
|
||||
@Override
|
||||
public boolean apply(Deck d) {
|
||||
for(Entry<DeckSection, CardPool> cp: d) {
|
||||
for(Entry<PaperCard, Integer> e : cp.getValue()) {
|
||||
if ( e.getKey().getRules().getAiHints().getRemAIDecks() )
|
||||
for (Entry<DeckSection, CardPool> cp: d) {
|
||||
for (Entry<PaperCard, Integer> e : cp.getValue()) {
|
||||
if (e.getKey().getRules().getAiHints().getRemAIDecks())
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -1198,7 +1246,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
String kws = params.get("AddKeyword");
|
||||
if (kws != null) {
|
||||
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
|
||||
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
|
||||
}
|
||||
if (bonusPT > 0) {
|
||||
threat = bonusPT * (1 + opp.getCreaturesInPlay().size()) / 10.0f;
|
||||
@@ -1212,7 +1260,7 @@ public class ComputerUtilCard {
|
||||
}
|
||||
|
||||
final float valueNow = Math.max(valueTempo, threat);
|
||||
if (valueNow < 0.2) { //hard floor to reduce ridiculous odds for instants over time
|
||||
if (valueNow < 0.2) { //hard floor to reduce ridiculous odds for instants over time
|
||||
return false;
|
||||
}
|
||||
final float chance = MyRandom.getRandom().nextFloat();
|
||||
@@ -1310,7 +1358,7 @@ public class ComputerUtilCard {
|
||||
threat *= 2;
|
||||
}
|
||||
if (c.getNetPower() == 0 && c == sa.getHostCard() && power > 0 ) {
|
||||
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;
|
||||
|
||||
@@ -1633,7 +1681,7 @@ public class ComputerUtilCard {
|
||||
pumped.addChangedCardKeywords(kws, null, false, false, timestamp);
|
||||
Set<CounterType> types = c.getCounters().keySet();
|
||||
for(CounterType ct : types) {
|
||||
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, true, null);
|
||||
pumped.addCounterFireNoEvents(ct, c.getCounters(ct), ai, sa, true, null);
|
||||
}
|
||||
//Copies tap-state and extra keywords (auras, equipment, etc.)
|
||||
if (c.isTapped()) {
|
||||
@@ -1644,11 +1692,12 @@ public class ComputerUtilCard {
|
||||
copiedKeywords.insertAll(pumped.getKeywords());
|
||||
List<KeywordInterface> toCopy = Lists.newArrayList();
|
||||
for (KeywordInterface k : c.getKeywords()) {
|
||||
if (!copiedKeywords.contains(k.getOriginal())) {
|
||||
if (k.getHidden()) {
|
||||
pumped.addHiddenExtrinsicKeyword(k);
|
||||
KeywordInterface copiedKI = k.copy(c, true);
|
||||
if (!copiedKeywords.contains(copiedKI.getOriginal())) {
|
||||
if (copiedKI.getHidden()) {
|
||||
pumped.addHiddenExtrinsicKeyword(copiedKI);
|
||||
} else {
|
||||
toCopy.add(k);
|
||||
toCopy.add(copiedKI);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1678,37 +1727,27 @@ public class ComputerUtilCard {
|
||||
// remove old boost that might be copied
|
||||
for (final StaticAbility stAb : c.getStaticAbilities()) {
|
||||
vCard.removePTBoost(c.getTimestamp(), stAb.getId());
|
||||
final Map<String, String> params = stAb.getMapParams();
|
||||
if (!params.get("Mode").equals("Continuous")) {
|
||||
if (!stAb.getParam("Mode").equals("Continuous")) {
|
||||
continue;
|
||||
}
|
||||
if (!params.containsKey("Affected")) {
|
||||
if (!stAb.hasParam("Affected")) {
|
||||
continue;
|
||||
}
|
||||
if (!params.containsKey("AddPower") && !params.containsKey("AddToughness")) {
|
||||
if (!stAb.hasParam("AddPower") && !stAb.hasParam("AddToughness")) {
|
||||
continue;
|
||||
}
|
||||
final String valid = params.get("Affected");
|
||||
if (!vCard.isValid(valid, c.getController(), c, null)) {
|
||||
if (!vCard.isValid(stAb.getParam("Affected").split(","), c.getController(), c, stAb)) {
|
||||
continue;
|
||||
}
|
||||
int att = 0;
|
||||
if (params.containsKey("AddPower")) {
|
||||
String addP = params.get("AddPower");
|
||||
if (addP.equals("AffectedX")) {
|
||||
att = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addP));
|
||||
} else {
|
||||
att = AbilityUtils.calculateAmount(c, addP, stAb);
|
||||
}
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
String addP = stAb.getParam("AddPower");
|
||||
att = AbilityUtils.calculateAmount(addP.startsWith("Affected") ? vCard : c, addP, stAb, true);
|
||||
}
|
||||
int def = 0;
|
||||
if (params.containsKey("AddToughness")) {
|
||||
String addT = params.get("AddToughness");
|
||||
if (addT.equals("AffectedY")) {
|
||||
def = CardFactoryUtil.xCount(vCard, AbilityUtils.getSVar(stAb, addT));
|
||||
} else {
|
||||
def = AbilityUtils.calculateAmount(c, addT, stAb);
|
||||
}
|
||||
if (stAb.hasParam("AddToughness")) {
|
||||
String addT = stAb.getParam("AddToughness");
|
||||
def = AbilityUtils.calculateAmount(addT.startsWith("Affected") ? vCard : c, addT, stAb, true);
|
||||
}
|
||||
vCard.addPTBoost(att, def, c.getTimestamp(), stAb.getId());
|
||||
}
|
||||
|
||||
@@ -33,7 +33,6 @@ import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
@@ -97,34 +96,39 @@ public class ComputerUtilCombat {
|
||||
* canAttackNextTurn.
|
||||
* </p>
|
||||
*
|
||||
* @param atacker
|
||||
* @param attacker
|
||||
* a {@link forge.game.card.Card} object.
|
||||
* @param defender
|
||||
* the defending {@link GameEntity}.
|
||||
* @return a boolean.
|
||||
*/
|
||||
public static boolean canAttackNextTurn(final Card atacker, final GameEntity defender) {
|
||||
if (!atacker.isCreature()) {
|
||||
public static boolean canAttackNextTurn(final Card attacker, final GameEntity defender) {
|
||||
if (!attacker.isCreature()) {
|
||||
return false;
|
||||
}
|
||||
if (!CombatUtil.canAttackNextTurn(atacker, defender)) {
|
||||
if (!CombatUtil.canAttackNextTurn(attacker, defender)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (final KeywordInterface inst : atacker.getKeywords()) {
|
||||
for (final KeywordInterface inst : attacker.getKeywords()) {
|
||||
final String keyword = inst.getOriginal();
|
||||
if (keyword.startsWith("CARDNAME attacks specific player each combat if able")) {
|
||||
final String defined = keyword.split(":")[1];
|
||||
final Player player = AbilityUtils.getDefinedPlayers(atacker, defined, null).get(0);
|
||||
final Player player = AbilityUtils.getDefinedPlayers(attacker, defined, null).get(0);
|
||||
if (!defender.equals(player)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO this should be a factor but needs some alignment with AttachAi
|
||||
//boolean leavesPlay = !ComputerUtilCard.hasActiveUndyingOrPersist(attacker)
|
||||
// && ((attacker.hasKeyword(Keyword.VANISHING) && attacker.getCounters(CounterEnumType.TIME) == 1)
|
||||
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
|
||||
// || attacker.hasSVar("EndOfTurnLeavePlay"));
|
||||
// The creature won't untap next turn
|
||||
return !atacker.isTapped() || Untap.canUntap(atacker);
|
||||
} // canAttackNextTurn(Card, GameEntity)
|
||||
return !attacker.isTapped() || Untap.canUntap(attacker);
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -330,7 +334,7 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
public static int resultingPoison(final Player ai, final Combat combat) {
|
||||
|
||||
// ai can't get poision counters, so the value can't change
|
||||
// ai can't get poison counters, so the value can't change
|
||||
if (!ai.canReceiveCounters(CounterEnumType.POISON)) {
|
||||
return ai.getPoisonCounters();
|
||||
}
|
||||
@@ -408,7 +412,6 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// check for creatures that must be blocked
|
||||
final List<Card> attackers = combat.getAttackersOf(ai);
|
||||
|
||||
@@ -488,8 +491,7 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
|
||||
public static boolean lifeInSeriousDanger(final Player ai, final Combat combat, final int payment) {
|
||||
// life in danger only cares about the player's life. Not about a
|
||||
// Planeswalkers life
|
||||
// life in danger only cares about the player's life. Not about a Planeswalkers life
|
||||
if (ai.cantLose() || combat == null) {
|
||||
return false;
|
||||
}
|
||||
@@ -564,8 +566,7 @@ public class ComputerUtilCombat {
|
||||
return damage;
|
||||
}
|
||||
|
||||
// This calculates the amount of damage a blocker in a blockgang can deal to
|
||||
// the attacker
|
||||
// This calculates the amount of damage a blocker in a blockgang can deal to the attacker
|
||||
/**
|
||||
* <p>
|
||||
* dealsDamageAsBlocker.
|
||||
@@ -578,7 +579,6 @@ public class ComputerUtilCombat {
|
||||
* @return a int.
|
||||
*/
|
||||
public static int dealsDamageAsBlocker(final Card attacker, final Card defender) {
|
||||
|
||||
int defenderDamage = predictDamageByBlockerWithoutDoubleStrike(attacker, defender);
|
||||
|
||||
if (defender.hasKeyword(Keyword.DOUBLE_STRIKE)) {
|
||||
@@ -643,7 +643,6 @@ public class ComputerUtilCombat {
|
||||
* @return a int.
|
||||
*/
|
||||
public static int totalShieldDamage(final Card attacker, final List<Card> defenders) {
|
||||
|
||||
int defenderDefense = 0;
|
||||
|
||||
for (final Card defender : defenders) {
|
||||
@@ -667,7 +666,6 @@ public class ComputerUtilCombat {
|
||||
* @return a int.
|
||||
*/
|
||||
public static int shieldDamage(final Card attacker, final Card blocker) {
|
||||
|
||||
if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) {
|
||||
return 0;
|
||||
}
|
||||
@@ -772,7 +770,6 @@ public class ComputerUtilCombat {
|
||||
public static boolean combatTriggerWillTrigger(final Card attacker, final Card defender, final Trigger trigger,
|
||||
Combat combat, final List<Card> plannedAttackers) {
|
||||
final Game game = attacker.getGame();
|
||||
final Map<String, String> trigParams = trigger.getMapParams();
|
||||
boolean willTrigger = false;
|
||||
final Card source = trigger.getHostCard();
|
||||
if (combat == null) {
|
||||
@@ -795,29 +792,27 @@ public class ComputerUtilCombat {
|
||||
if (combat.isAttacking(attacker)) {
|
||||
return false; // The trigger should have triggered already
|
||||
}
|
||||
if (trigParams.containsKey("ValidCard")) {
|
||||
if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))
|
||||
&& !(combat.isAttacking(source) && trigger.matchesValid(source,
|
||||
trigParams.get("ValidCard").split(","))
|
||||
&& !trigParams.containsKey("Alone"))) {
|
||||
if (trigger.hasParam("ValidCard")) {
|
||||
if (!trigger.matchesValidParam("ValidCard", attacker)
|
||||
&& !(combat.isAttacking(source) && trigger.matchesValidParam("ValidCard", source)
|
||||
&& !trigger.hasParam("Alone"))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (trigParams.containsKey("Attacked")) {
|
||||
if (combat.isAttacking(attacker)) {
|
||||
GameEntity attacked = combat.getDefenderByAttacker(attacker);
|
||||
if (!trigger.matchesValid(attacked, trigParams.get("Attacked").split(","))) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if ("You,Planeswalker.YouCtrl".equals(trigParams.get("Attacked"))) {
|
||||
if (source.getController() == attacker.getController()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trigger.hasParam("Attacked")) {
|
||||
if (combat.isAttacking(attacker)) {
|
||||
if (!trigger.matchesValidParam("Attacked", combat.getDefenderByAttacker(attacker))) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if ("You,Planeswalker.YouCtrl".equals(trigger.getParam("Attacked"))) {
|
||||
if (source.getController() == attacker.getController()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (trigParams.containsKey("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) {
|
||||
if (trigger.hasParam("Alone") && plannedAttackers != null && plannedAttackers.size() != 1) {
|
||||
return false; // won't trigger since the AI is planning to attack with more than one creature
|
||||
}
|
||||
}
|
||||
@@ -825,10 +820,8 @@ public class ComputerUtilCombat {
|
||||
// defender == null means unblocked
|
||||
if ((defender == null) && mode == TriggerType.AttackerUnblocked) {
|
||||
willTrigger = true;
|
||||
if (trigParams.containsKey("ValidCard")) {
|
||||
if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
|
||||
return false;
|
||||
}
|
||||
if (!trigger.matchesValidParam("ValidCard", attacker)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -838,8 +831,8 @@ public class ComputerUtilCombat {
|
||||
|
||||
if (mode == TriggerType.Blocks) {
|
||||
willTrigger = true;
|
||||
if (trigParams.containsKey("ValidBlocked")) {
|
||||
String validBlocked = trigParams.get("ValidBlocked");
|
||||
if (trigger.hasParam("ValidBlocked")) {
|
||||
String validBlocked = trigger.getParam("ValidBlocked");
|
||||
if (validBlocked.contains(".withLesserPower")) {
|
||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
||||
// ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE
|
||||
@@ -852,8 +845,8 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (trigParams.containsKey("ValidCard")) {
|
||||
String validBlocker = trigParams.get("ValidCard");
|
||||
if (trigger.hasParam("ValidCard")) {
|
||||
String validBlocker = trigger.getParam("ValidCard");
|
||||
if (validBlocker.contains(".withLesserPower")) {
|
||||
// Have to check this restriction here as triggering objects aren't set yet, so
|
||||
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
|
||||
@@ -868,29 +861,23 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
} else if (mode == TriggerType.AttackerBlocked || mode == TriggerType.AttackerBlockedByCreature) {
|
||||
willTrigger = true;
|
||||
if (trigParams.containsKey("ValidBlocker")) {
|
||||
if (!trigger.matchesValid(defender, trigParams.get("ValidBlocker").split(","))) {
|
||||
return false;
|
||||
}
|
||||
if (!trigger.matchesValidParam("ValidBlocker", defender)) {
|
||||
return false;
|
||||
}
|
||||
if (trigParams.containsKey("ValidCard")) {
|
||||
if (!trigger.matchesValid(attacker, trigParams.get("ValidCard").split(","))) {
|
||||
return false;
|
||||
}
|
||||
if (!trigger.matchesValidParam("ValidCard", attacker)) {
|
||||
return false;
|
||||
}
|
||||
} else if (mode == TriggerType.DamageDone) {
|
||||
willTrigger = true;
|
||||
if (trigParams.containsKey("ValidSource")) {
|
||||
if (!(trigger.matchesValid(defender, trigParams.get("ValidSource").split(","))
|
||||
if (trigger.hasParam("ValidSource")) {
|
||||
if (!(trigger.matchesValidParam("ValidSource", defender)
|
||||
&& defender.getNetCombatDamage() > 0
|
||||
&& (!trigParams.containsKey("ValidTarget")
|
||||
|| trigger.matchesValid(attacker, trigParams.get("ValidTarget").split(","))))) {
|
||||
&& trigger.matchesValidParam("ValidTarget", attacker))) {
|
||||
return false;
|
||||
}
|
||||
if (!(trigger.matchesValid(attacker, trigParams.get("ValidSource").split(","))
|
||||
if (!(trigger.matchesValidParam("ValidSource", attacker)
|
||||
&& attacker.getNetCombatDamage() > 0
|
||||
&& (!trigParams.containsKey("ValidTarget")
|
||||
|| trigger.matchesValid(defender, trigParams.get("ValidTarget").split(","))))) {
|
||||
&& trigger.matchesValidParam("ValidTarget", defender))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -939,30 +926,22 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
|
||||
final Game game = attacker.getGame();
|
||||
// look out for continuous static abilities that only care for blocking
|
||||
// creatures
|
||||
// look out for continuous static abilities that only care for blocking creatures
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
final Map<String, String> params = stAb.getMapParams();
|
||||
if (!params.get("Mode").equals("Continuous")) {
|
||||
if (!stAb.getParam("Mode").equals("Continuous")) {
|
||||
continue;
|
||||
}
|
||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) {
|
||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
|
||||
continue;
|
||||
}
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "blocking", "Creature");
|
||||
if (!blocker.isValid(valid, card.getController(), card, null)) {
|
||||
final String valid = TextUtil.fastReplace(stAb.getParam("Affected"), "blocking", "Creature");
|
||||
if (!blocker.isValid(valid, card.getController(), card, stAb)) {
|
||||
continue;
|
||||
}
|
||||
if (params.containsKey("AddPower")) {
|
||||
if (params.get("AddPower").equals("X")) {
|
||||
power += CardFactoryUtil.xCount(card, card.getSVar("X"));
|
||||
} else if (params.get("AddPower").equals("Y")) {
|
||||
power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
|
||||
} else {
|
||||
power += Integer.valueOf(params.get("AddPower"));
|
||||
}
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
power += AbilityUtils.calculateAmount(card, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1247,31 +1226,23 @@ public class ComputerUtilCombat {
|
||||
theTriggers.addAll(blocker.getTriggers());
|
||||
}
|
||||
|
||||
// look out for continuous static abilities that only care for attacking
|
||||
// creatures
|
||||
// look out for continuous static abilities that only care for attacking creatures
|
||||
if (!withoutCombatStaticAbilities) {
|
||||
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
|
||||
for (final Card card : cardList) {
|
||||
for (final StaticAbility stAb : card.getStaticAbilities()) {
|
||||
final Map<String, String> params = stAb.getMapParams();
|
||||
if (!params.get("Mode").equals("Continuous")) {
|
||||
if (!stAb.getParam("Mode").equals("Continuous")) {
|
||||
continue;
|
||||
}
|
||||
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
|
||||
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
|
||||
continue;
|
||||
}
|
||||
final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, null)) {
|
||||
final String valid = TextUtil.fastReplace(stAb.getParam("Affected"), "attacking", "Creature");
|
||||
if (!attacker.isValid(valid, card.getController(), card, stAb)) {
|
||||
continue;
|
||||
}
|
||||
if (params.containsKey("AddPower")) {
|
||||
if (params.get("AddPower").equals("X")) {
|
||||
power += CardFactoryUtil.xCount(card, card.getSVar("X"));
|
||||
} else if (params.get("AddPower").equals("Y")) {
|
||||
power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
|
||||
} else {
|
||||
power += Integer.valueOf(params.get("AddPower"));
|
||||
}
|
||||
if (stAb.hasParam("AddPower")) {
|
||||
power += AbilityUtils.calculateAmount(card, stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1345,7 +1316,7 @@ public class ComputerUtilCombat {
|
||||
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness())));
|
||||
}
|
||||
power += CardFactoryUtil.xCount(source, bonus);
|
||||
power += AbilityUtils.calculateAmount(source, bonus, sa);
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1442,8 +1413,7 @@ public class ComputerUtilCombat {
|
||||
theTriggers.addAll(blocker.getTriggers());
|
||||
}
|
||||
|
||||
// look out for continuous static abilities that only care for attacking
|
||||
// creatures
|
||||
// look out for continuous static abilities that only care for attacking creatures
|
||||
if (!withoutCombatStaticAbilities) {
|
||||
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
|
||||
for (final Card card : cardList) {
|
||||
@@ -1535,7 +1505,7 @@ public class ComputerUtilCombat {
|
||||
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
}
|
||||
toughness += CardFactoryUtil.xCount(source, bonus);
|
||||
toughness += AbilityUtils.calculateAmount(source, bonus, sa);
|
||||
}
|
||||
} else if (ApiType.PumpAll.equals(sa.getApi())) {
|
||||
|
||||
@@ -1568,7 +1538,7 @@ public class ComputerUtilCombat {
|
||||
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
|
||||
bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
|
||||
}
|
||||
toughness += CardFactoryUtil.xCount(source, bonus);
|
||||
toughness += AbilityUtils.calculateAmount(source, bonus, sa);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1633,8 +1603,8 @@ public class ComputerUtilCombat {
|
||||
|
||||
//Check triggers that deal damage or shrink the attacker
|
||||
if (ComputerUtilCombat.getDamageToKill(attacker)
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities) <= 0) {
|
||||
return true;
|
||||
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities) <= 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check Destroy triggers (Cockatrice and friends)
|
||||
@@ -2251,11 +2221,12 @@ public class ComputerUtilCombat {
|
||||
* @return a int.
|
||||
*/
|
||||
public final static int getDamageToKill(final Card c) {
|
||||
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
|
||||
int damageShield = c.getPreventNextDamageTotalShields();
|
||||
int killDamage = c.getLethalDamage() + damageShield;
|
||||
|
||||
if ((killDamage > c.getPreventNextDamageTotalShields())
|
||||
if ((killDamage > damageShield)
|
||||
&& c.hasSVar("DestroyWhenDamaged")) {
|
||||
killDamage = 1 + c.getPreventNextDamageTotalShields();
|
||||
killDamage = 1 + damageShield;
|
||||
}
|
||||
|
||||
return killDamage;
|
||||
@@ -2277,7 +2248,6 @@ public class ComputerUtilCombat {
|
||||
*/
|
||||
|
||||
public final static int predictDamageTo(final Player target, final int damage, final Card source, final boolean isCombat) {
|
||||
|
||||
final Game game = target.getGame();
|
||||
int restDamage = damage;
|
||||
|
||||
@@ -2286,39 +2256,38 @@ public class ComputerUtilCombat {
|
||||
// Predict replacement effects
|
||||
for (final Card ca : game.getCardsIn(ZoneType.STATIC_ABILITIES_SOURCE_ZONES)) {
|
||||
for (final ReplacementEffect re : ca.getReplacementEffects()) {
|
||||
Map<String, String> params = re.getMapParams();
|
||||
if (!re.getMode().equals(ReplacementType.DamageDone) || !params.containsKey("PreventionEffect")) {
|
||||
if (!re.getMode().equals(ReplacementType.DamageDone) ||
|
||||
(!re.hasParam("PreventionEffect") && !re.hasParam("Prevent"))) {
|
||||
continue;
|
||||
}
|
||||
// Immortal Coil prevents the damage but has a similar negative effect
|
||||
if ("Immortal Coil".equals(ca.getName())) {
|
||||
continue;
|
||||
}
|
||||
if (params.containsKey("ValidSource")
|
||||
&& !source.isValid(params.get("ValidSource"), ca.getController(), ca, null)) {
|
||||
if (!re.matchesValidParam("ValidSource", source)) {
|
||||
continue;
|
||||
}
|
||||
if (params.containsKey("ValidTarget")
|
||||
&& !target.isValid(params.get("ValidTarget"), ca.getController(), ca, null)) {
|
||||
if (!re.matchesValidParam("ValidTarget", source)) {
|
||||
continue;
|
||||
}
|
||||
if (params.containsKey("IsCombat")) {
|
||||
if (params.get("IsCombat").equals("True")) {
|
||||
if (!isCombat) {
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
if (isCombat) {
|
||||
continue;
|
||||
}
|
||||
if (re.hasParam("IsCombat")) {
|
||||
if (re.getParam("IsCombat").equals("True") != isCombat) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (re.hasParam("Prevent")) {
|
||||
return 0;
|
||||
} else if (re.getOverridingAbility() != null) {
|
||||
SpellAbility repSA = re.getOverridingAbility();
|
||||
if (repSA.getApi() == ApiType.ReplaceDamage) {
|
||||
return Math.max(0, restDamage - AbilityUtils.calculateAmount(ca, repSA.getParam("Amount"), repSA));
|
||||
}
|
||||
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
restDamage = target.staticDamagePrevention(restDamage, source, isCombat, true);
|
||||
restDamage = target.staticDamagePrevention(restDamage, 0, source, isCombat);
|
||||
|
||||
return restDamage;
|
||||
}
|
||||
@@ -2339,13 +2308,7 @@ public class ComputerUtilCombat {
|
||||
// This function helps the AI calculate the actual amount of damage an
|
||||
// effect would deal
|
||||
public final static int predictDamageTo(final Card target, final int damage, final Card source, final boolean isCombat) {
|
||||
|
||||
int restDamage = damage;
|
||||
|
||||
restDamage = target.staticReplaceDamage(restDamage, source, isCombat);
|
||||
restDamage = target.staticDamagePrevention(restDamage, source, isCombat, true);
|
||||
|
||||
return restDamage;
|
||||
return predictDamageTo(target, damage, 0, source, isCombat);
|
||||
}
|
||||
|
||||
|
||||
@@ -2367,7 +2330,6 @@ public class ComputerUtilCombat {
|
||||
* @return a int.
|
||||
*/
|
||||
public final static int predictDamageTo(final Card target, final int damage, final int possiblePrevention, final Card source, final boolean isCombat) {
|
||||
|
||||
int restDamage = damage;
|
||||
|
||||
restDamage = target.staticReplaceDamage(restDamage, source, isCombat);
|
||||
@@ -2377,7 +2339,6 @@ public class ComputerUtilCombat {
|
||||
}
|
||||
|
||||
public final static boolean dealsFirstStrikeDamage(final Card combatant, final boolean withoutAbilities, final Combat combat) {
|
||||
|
||||
if (combatant.hasKeyword(Keyword.DOUBLE_STRIKE) || combatant.hasKeyword(Keyword.FIRST_STRIKE)) {
|
||||
return true;
|
||||
}
|
||||
@@ -2485,7 +2446,14 @@ public class ComputerUtilCombat {
|
||||
List<ReplacementEffect> list = game.getReplacementHandler().getReplacementList(
|
||||
ReplacementType.DamageDone, repParams, ReplacementLayer.Other);
|
||||
|
||||
return !list.isEmpty();
|
||||
for (final ReplacementEffect re : list) {
|
||||
Map<String, String> params = re.getMapParams();
|
||||
if (params.containsKey("Prevent") ||
|
||||
(re.getOverridingAbility() != null && re.getOverridingAbility().getApi() != ApiType.ReplaceDamage && re.getOverridingAbility().getApi() != ApiType.ReplaceEffect)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean attackerHasThreateningAfflict(Card attacker, Player aiDefender) {
|
||||
@@ -2494,20 +2462,6 @@ public class ComputerUtilCombat {
|
||||
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
|
||||
}
|
||||
|
||||
public static int getMaxAttackersFor(final GameEntity defender) {
|
||||
if (defender instanceof Player) {
|
||||
for (final Card card : ((Player) defender).getCardsIn(ZoneType.Battlefield)) {
|
||||
if (card.hasKeyword("No more than one creature can attack you each combat.")) {
|
||||
return 1;
|
||||
} else if (card.hasKeyword("No more than two creatures can attack you each combat.")) {
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
public static List<Card> categorizeAttackersByEvasion(List<Card> attackers) {
|
||||
List<Card> categorizedAttackers = Lists.newArrayList();
|
||||
|
||||
@@ -2597,5 +2551,3 @@ public class ComputerUtilCombat {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import forge.game.ability.ApiType;
|
||||
import org.apache.commons.lang3.ObjectUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -137,9 +138,8 @@ public class ComputerUtilCost {
|
||||
* the source
|
||||
* @return true, if successful
|
||||
*/
|
||||
|
||||
public static boolean checkDiscardCost(final Player ai, final Cost cost, final Card source, SpellAbility sa) {
|
||||
if (cost == null || source.hasSVar("AISkipDiscardCostCheck") /* FIXME: should not be needed! */ ) {
|
||||
if (cost == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -157,7 +157,7 @@ public class ComputerUtilCost {
|
||||
if (typeList.size() > ai.getMaxHandSize()) {
|
||||
continue;
|
||||
}
|
||||
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), null);
|
||||
int num = AbilityUtils.calculateAmount(source, disc.getAmount(), sa);
|
||||
|
||||
for (int i = 0; i < num; i++) {
|
||||
Card pref = ComputerUtil.getCardPreference(ai, source, "DiscardCost", typeList);
|
||||
@@ -338,17 +338,17 @@ public class ComputerUtilCost {
|
||||
}
|
||||
|
||||
public static boolean isSacrificeSelfCost(final Cost cost) {
|
||||
if (cost == null) {
|
||||
return false;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
if ("CARDNAME".equals(part.getType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
if (cost == null) {
|
||||
return false;
|
||||
}
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostSacrifice) {
|
||||
if ("CARDNAME".equals(part.getType())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -368,13 +368,13 @@ public class ComputerUtilCost {
|
||||
if (part instanceof CostTapType) {
|
||||
/*
|
||||
* Only crew with creatures weaker than vehicle
|
||||
*
|
||||
*
|
||||
* Possible improvements:
|
||||
* - block against evasive (flyers, intimidate, etc.)
|
||||
* - break board stall by racing with evasive vehicle
|
||||
*/
|
||||
if (sa.hasParam("Crew")) {
|
||||
Card vehicle = AnimateAi.becomeAnimated(source, sa);
|
||||
Card vehicle = AnimateAi.becomeAnimated(source, sa);
|
||||
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
|
||||
String type = part.getType();
|
||||
String totalP = type.split("withTotalPowerGE")[1];
|
||||
@@ -391,7 +391,7 @@ public class ComputerUtilCost {
|
||||
return ComputerUtil.chooseTapTypeAccumulatePower(ai, type, sa, true,
|
||||
Integer.parseInt(totalP), exclude) != null;
|
||||
}
|
||||
return false;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
@@ -479,9 +479,9 @@ public class ComputerUtilCost {
|
||||
}
|
||||
}
|
||||
for (Card c : player.getCardsIn(ZoneType.Command)) {
|
||||
if (cannotBeCountered) {
|
||||
continue;
|
||||
}
|
||||
if (cannotBeCountered) {
|
||||
continue;
|
||||
}
|
||||
final String snem = c.getSVar("SpellsNeedExtraManaEffect");
|
||||
if (!StringUtils.isBlank(snem)) {
|
||||
if (StringUtils.isNumeric(snem)) {
|
||||
@@ -549,7 +549,7 @@ public class ComputerUtilCost {
|
||||
}
|
||||
|
||||
return ComputerUtilMana.canPayManaCost(sa, player, extraManaNeeded)
|
||||
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
|
||||
&& CostPayment.canPayAdditionalCosts(sa.getPayCosts(), sa);
|
||||
} // canPayCost()
|
||||
|
||||
public static boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
|
||||
@@ -558,7 +558,6 @@ public class ComputerUtilCost {
|
||||
boolean payForOwnOnly = "OnlyOwn".equals(aiLogic);
|
||||
boolean payOwner = sa.hasParam("UnlessAI") && aiLogic.startsWith("Defined");
|
||||
boolean payNever = "Never".equals(aiLogic);
|
||||
boolean shockland = "Shockland".equals(aiLogic);
|
||||
boolean isMine = sa.getActivatingPlayer().equals(payer);
|
||||
|
||||
if (payNever) { return false; }
|
||||
@@ -573,26 +572,6 @@ public class ComputerUtilCost {
|
||||
if (sa.getHostCard() == null || payer.equals(sa.getHostCard().getController())) {
|
||||
return false;
|
||||
}
|
||||
} else if (shockland) {
|
||||
if (payer.getLife() > 3 && payer.canPayLife(2)) {
|
||||
final int landsize = payer.getLandsInPlay().size() + 1;
|
||||
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
|
||||
// if the new land size would equal the CMC of a card in AIs hand, consider playing it untapped,
|
||||
// otherwise don't bother running other checks
|
||||
if (landsize != c.getCMC()) {
|
||||
continue;
|
||||
}
|
||||
// try to determine in the AI is actually planning to play a spell ability from the card
|
||||
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
|
||||
// try to determine if the mana shards provided by the lands would be applicable to pay the mana cost
|
||||
boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
|
||||
|
||||
if (canPay && willPlay) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if ("Paralyze".equals(aiLogic)) {
|
||||
final Card c = source.getEnchantingCard();
|
||||
if (c == null || c.isUntapped()) {
|
||||
@@ -610,10 +589,10 @@ public class ComputerUtilCost {
|
||||
return false;
|
||||
}
|
||||
} else if (aiLogic != null && aiLogic.startsWith("LifeLE")) {
|
||||
// if payer can't lose life its no need to pay unless
|
||||
if (!payer.canLoseLife())
|
||||
return false;
|
||||
else if (payer.getLife() <= Integer.valueOf(aiLogic.substring(6))) {
|
||||
// if payer can't lose life its no need to pay unless
|
||||
if (!payer.canLoseLife())
|
||||
return false;
|
||||
else if (payer.getLife() <= AbilityUtils.calculateAmount(source, aiLogic.substring(6), sa)) {
|
||||
return true;
|
||||
}
|
||||
} else if ("WillAttack".equals(aiLogic)) {
|
||||
@@ -630,6 +609,29 @@ public class ComputerUtilCost {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for shocklands and similar ETB replacement effects
|
||||
if (sa.hasParam("ETB") && sa.getApi().equals(ApiType.Tap)) {
|
||||
for (final CostPart part : cost.getCostParts()) {
|
||||
if (part instanceof CostPayLife) {
|
||||
final CostPayLife lifeCost = (CostPayLife) part;
|
||||
Integer amount = lifeCost.convertAmount();
|
||||
if (payer.getLife() > (amount + 1) && payer.canPayLife(amount)) {
|
||||
final int landsize = payer.getLandsInPlay().size() + 1;
|
||||
for (Card c : payer.getCardsIn(ZoneType.Hand)) {
|
||||
// Check if the AI has enough lands to play the card
|
||||
if (landsize != c.getCMC()) {
|
||||
continue;
|
||||
}
|
||||
// Check if the AI intends to play the card and if it can pay for it with the mana it has
|
||||
boolean willPlay = ComputerUtil.hasReasonToPlayCardThisTurn(payer, c);
|
||||
boolean canPay = c.getManaCost().canBePaidWithAvaliable(ColorSet.fromNames(getAvailableManaColors(payer, source)).getColor());
|
||||
return canPay && willPlay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AI will only pay when it's not already payed and only opponents abilities
|
||||
if (alreadyPaid || (payers.size() > 1 && (isMine && !payForOwnOnly))) {
|
||||
return false;
|
||||
@@ -639,13 +641,13 @@ public class ComputerUtilCost {
|
||||
// Didn't have any of the data on the original SA to pay dependant costs
|
||||
|
||||
return checkLifeCost(payer, cost, source, 4, sa)
|
||||
&& checkDamageCost(payer, cost, source, 4)
|
||||
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || checkDiscardCost(payer, cost, source, sa))
|
||||
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
||||
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
|
||||
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
|
||||
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||
&& checkDamageCost(payer, cost, source, 4)
|
||||
&& (isMine || checkSacrificeCost(payer, cost, source, sa))
|
||||
&& (isMine || checkDiscardCost(payer, cost, source, sa))
|
||||
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
|
||||
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
|
||||
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
|
||||
&& (!source.getName().equals("Chain of Vapor") || (payer.getWeakestOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
|
||||
}
|
||||
|
||||
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {
|
||||
@@ -685,14 +687,14 @@ public class ComputerUtilCost {
|
||||
public static int getMaxXValue(SpellAbility sa, Player ai) {
|
||||
final Card source = sa.getHostCard();
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Cost abCost = root.getPayCosts();
|
||||
if (abCost == null || !abCost.hasXInAnyCostPart()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
Integer val = null;
|
||||
|
||||
if (sa.costHasManaX()) {
|
||||
if (root.costHasManaX()) {
|
||||
val = ComputerUtilMana.determineLeftoverMana(root, ai);
|
||||
}
|
||||
|
||||
|
||||
@@ -319,8 +319,8 @@ public class ComputerUtilMana {
|
||||
|
||||
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
|
||||
// to attempt to make the spell uncounterable when possible.
|
||||
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls") && ma.hasChosenType()
|
||||
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getChosenType(0))) {
|
||||
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
|
||||
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
|
||||
if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
|
||||
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
|
||||
continue;
|
||||
@@ -341,7 +341,7 @@ public class ComputerUtilMana {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts)) {
|
||||
if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts, cost.getXManaCostPaidByColor())) {
|
||||
return paymentChoice;
|
||||
}
|
||||
}
|
||||
@@ -414,6 +414,7 @@ public class ComputerUtilMana {
|
||||
// then apply this one
|
||||
if (!replaceType.isEmpty()) {
|
||||
for (SpellAbility saMana : replaceAmount) {
|
||||
Card card = saMana.getHostCard();
|
||||
if (saMana.hasParam("ReplaceType")) {
|
||||
// replace color and colorless
|
||||
String color = saMana.getParam("ReplaceType");
|
||||
@@ -435,8 +436,8 @@ public class ComputerUtilMana {
|
||||
// replace color
|
||||
String color = saMana.getParam("ReplaceColor");
|
||||
if ("Chosen".equals(color)) {
|
||||
if (saMana.hasChosenColor()) {
|
||||
color = MagicColor.toShortString(saMana.getChosenColor());
|
||||
if (card.hasChosenColor()) {
|
||||
color = MagicColor.toShortString(card.getChosenColor());
|
||||
}
|
||||
}
|
||||
if (saMana.hasParam("ReplaceOnly")) {
|
||||
@@ -488,7 +489,7 @@ public class ComputerUtilMana {
|
||||
int pAmount = AbilityUtils.calculateAmount(trSA.getHostCard(), trSA.getParamOrDefault("Amount", "1"), trSA);
|
||||
String produced = trSA.getParam("Produced");
|
||||
if (produced.equals("Chosen")) {
|
||||
produced = MagicColor.toShortString(trSA.getHostCard().getChosenColor(trSA));
|
||||
produced = MagicColor.toShortString(trSA.getHostCard().getChosenColor());
|
||||
}
|
||||
manaProduced += " " + StringUtils.repeat(produced, pAmount);
|
||||
} else if (ApiType.ManaReflected.equals(trSA.getApi())) {
|
||||
@@ -546,7 +547,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// get a mana of this type from floating, bail if none available
|
||||
final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), (byte) -1);
|
||||
final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), (byte) -1, cost.getXManaCostPaidByColor());
|
||||
if (mana != null) {
|
||||
if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, false)) {
|
||||
manaSpentToPay.add(0, mana);
|
||||
@@ -608,7 +609,6 @@ public class ComputerUtilMana {
|
||||
|
||||
String manaProduced = predictManafromSpellAbility(saPayment, ai, toPay);
|
||||
|
||||
//System.out.println(manaProduced);
|
||||
payMultipleMana(cost, manaProduced, ai);
|
||||
|
||||
// remove from available lists
|
||||
@@ -935,7 +935,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
// get a mana of this type from floating, bail if none available
|
||||
final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), hasConverge ? cost.getColorsPaid() : -1);
|
||||
final Mana mana = getMana(ai, part, sa, cost.getSourceRestriction(), hasConverge ? cost.getColorsPaid() : -1, cost.getXManaCostPaidByColor());
|
||||
if (mana != null) {
|
||||
if (ai.getManaPool().tryPayCostWithMana(sa, cost, mana, test)) {
|
||||
manaSpentToPay.add(0, mana);
|
||||
@@ -964,8 +964,10 @@ public class ComputerUtilMana {
|
||||
* a {@link forge.game.spellability.SpellAbility} object.
|
||||
* @return a {@link forge.game.mana.Mana} object.
|
||||
*/
|
||||
private static Mana getMana(final Player ai, final ManaCostShard shard, final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid) {
|
||||
final List<Pair<Mana, Integer>> weightedOptions = selectManaToPayFor(ai.getManaPool(), shard, saBeingPaidFor, restriction, colorsPaid);
|
||||
private static Mana getMana(final Player ai, final ManaCostShard shard, final SpellAbility saBeingPaidFor,
|
||||
String restriction, final byte colorsPaid, Map<String, Integer> xManaCostPaidByColor) {
|
||||
final List<Pair<Mana, Integer>> weightedOptions = selectManaToPayFor(ai.getManaPool(), shard,
|
||||
saBeingPaidFor, restriction, colorsPaid, xManaCostPaidByColor);
|
||||
|
||||
// Exclude border case
|
||||
if (weightedOptions.isEmpty()) {
|
||||
@@ -1014,9 +1016,13 @@ public class ComputerUtilMana {
|
||||
}
|
||||
|
||||
private static List<Pair<Mana, Integer>> selectManaToPayFor(final ManaPool manapool, final ManaCostShard shard,
|
||||
final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid) {
|
||||
final SpellAbility saBeingPaidFor, String restriction, final byte colorsPaid, Map<String, Integer> xManaCostPaidByColor) {
|
||||
final List<Pair<Mana, Integer>> weightedOptions = new ArrayList<>();
|
||||
for (final Mana thisMana : manapool) {
|
||||
if (shard == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(thisMana.getColor()), xManaCostPaidByColor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!manapool.canPayForShardWithColor(shard, thisMana.getColor())) {
|
||||
continue;
|
||||
}
|
||||
@@ -1092,7 +1098,7 @@ public class ComputerUtilMana {
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean canPayShardWithSpellAbility(ManaCostShard toPay, Player ai, SpellAbility ma, SpellAbility sa, boolean checkCosts) {
|
||||
private static boolean canPayShardWithSpellAbility(ManaCostShard toPay, Player ai, SpellAbility ma, SpellAbility sa, boolean checkCosts, Map<String, Integer> xManaCostPaidByColor) {
|
||||
final Card sourceCard = ma.getHostCard();
|
||||
|
||||
if (isManaSourceReserved(ai, sourceCard, sa)) {
|
||||
@@ -1130,6 +1136,10 @@ public class ComputerUtilMana {
|
||||
|
||||
if (m.isComboMana()) {
|
||||
for (String s : m.getComboColors().split(" ")) {
|
||||
if (toPay == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(s, xManaCostPaidByColor)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s)))
|
||||
return true;
|
||||
}
|
||||
@@ -1140,6 +1150,9 @@ public class ComputerUtilMana {
|
||||
Set<String> reflected = CardUtil.getReflectableManaColors(ma);
|
||||
|
||||
for (byte c : MagicColor.WUBRG) {
|
||||
if (toPay == ManaCostShard.COLORED_X && !ManaCostBeingPaid.canColoredXShardBePaidByColor(MagicColor.toShortString(c), xManaCostPaidByColor)) {
|
||||
continue;
|
||||
}
|
||||
if (ai.getManaPool().canPayForShardWithColor(toPay, c) && reflected.contains(MagicColor.toLongString(c))) {
|
||||
m.setExpressChoice(MagicColor.toShortString(c));
|
||||
return true;
|
||||
@@ -1147,6 +1160,16 @@ public class ComputerUtilMana {
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (toPay == ManaCostShard.COLORED_X) {
|
||||
for (String s : m.mana().split(" ")) {
|
||||
if (ManaCostBeingPaid.canColoredXShardBePaidByColor(s, xManaCostPaidByColor)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1433,17 +1456,26 @@ public class ComputerUtilMana {
|
||||
// Tack xMana Payments into mana here if X is a set value
|
||||
if (cost.getXcounter() > 0 || extraMana > 0) {
|
||||
int manaToAdd = 0;
|
||||
int xCounter = cost.getXcounter();
|
||||
if (test && extraMana > 0) {
|
||||
final int multiplicator = Math.max(cost.getXcounter(), 1);
|
||||
final int multiplicator = Math.max(xCounter, 1);
|
||||
manaToAdd = extraMana * multiplicator;
|
||||
} else {
|
||||
manaToAdd = AbilityUtils.calculateAmount(card, "X", sa) * cost.getXcounter();
|
||||
manaToAdd = AbilityUtils.calculateAmount(card, "X", sa) * xCounter;
|
||||
}
|
||||
|
||||
cost.increaseShard(ManaCostShard.parseNonGeneric(sa.getParamOrDefault("XColor", "1")), manaToAdd);
|
||||
String xColor = sa.getParamOrDefault("XColor", "1");
|
||||
if (card.hasKeyword("Spend only colored mana on X. No more than one mana of each color may be spent this way.")) {
|
||||
xColor = "WUBRGX";
|
||||
}
|
||||
if (xCounter > 0) {
|
||||
cost.setXManaCostPaid(manaToAdd / xCounter, xColor);
|
||||
} else {
|
||||
cost.increaseShard(ManaCostShard.parseNonGeneric(xColor), manaToAdd);
|
||||
}
|
||||
|
||||
if (!test) {
|
||||
sa.setXManaCostPaid(manaToAdd / cost.getXcounter());
|
||||
sa.setXManaCostPaid(manaToAdd / xCounter);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1530,7 +1562,7 @@ public class ComputerUtilMana {
|
||||
public boolean apply(final Card c) {
|
||||
for (final SpellAbility am : getAIPlayableMana(c)) {
|
||||
am.setActivatingPlayer(ai);
|
||||
if (!checkPlayable || am.canPlay()) {
|
||||
if (!checkPlayable || (am.canPlay() && am.checkRestrictions(ai))) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,9 +162,9 @@ public class CreatureEvaluator implements Function<Card, Integer> {
|
||||
value -= subValue(10, "must-attack");
|
||||
} else if (c.hasStartOfKeyword("CARDNAME attacks specific player each combat if able")) {
|
||||
value -= subValue(10, "must-attack-player");
|
||||
} else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
|
||||
}/* else if (c.hasKeyword("CARDNAME can block only creatures with flying.")) {
|
||||
value -= subValue(toughness * 5, "reverse-reach");
|
||||
}
|
||||
}//*/
|
||||
|
||||
if (c.hasSVar("DestroyWhenDamaged")) {
|
||||
value -= subValue((toughness - 1) * 9, "dies-to-dmg");
|
||||
|
||||
@@ -3,23 +3,12 @@ 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.*;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
|
||||
import com.google.common.collect.*;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import com.google.common.collect.ArrayListMultimap;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
import com.google.common.collect.Multimap;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.card.CardStateName;
|
||||
import forge.card.MagicColor;
|
||||
@@ -27,7 +16,6 @@ import forge.game.Game;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.GameObject;
|
||||
import forge.game.ability.AbilityFactory;
|
||||
import forge.game.ability.AbilityKey;
|
||||
import forge.game.ability.effects.DetachedCardEffect;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCloneStates;
|
||||
@@ -44,9 +32,7 @@ import forge.game.mana.ManaPool;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilityManaPart;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.PlayerZone;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.item.IPaperCard;
|
||||
@@ -89,7 +75,8 @@ public abstract class GameState {
|
||||
private final Map<Card, Integer> markedDamage = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
|
||||
private final Map<Card, CardCollection> cardToChosenCards = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToChosenTypes = new HashMap<>();
|
||||
private final Map<Card, String> cardToChosenType = new HashMap<>();
|
||||
private final Map<Card, String> cardToChosenType2 = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
|
||||
private final Map<Card, List<String>> cardToMergedCards = new HashMap<>();
|
||||
@@ -340,16 +327,15 @@ public abstract class GameState {
|
||||
newText.append("|Damage:").append(c.getDamage());
|
||||
}
|
||||
|
||||
SpellAbility first = c.getFirstSpellAbility();
|
||||
if (first != null) {
|
||||
if (first.hasChosenColor()) {
|
||||
newText.append("|ChosenColor:").append(TextUtil.join(first.getChosenColors(), ","));
|
||||
}
|
||||
if (first.hasChosenType()) {
|
||||
newText.append("|ChosenType:").append(TextUtil.join(first.getChosenType(), ","));
|
||||
}
|
||||
if (!c.getChosenColor().isEmpty()) {
|
||||
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ","));
|
||||
}
|
||||
if (!c.getChosenType().isEmpty()) {
|
||||
newText.append("|ChosenType:").append(c.getChosenType());
|
||||
}
|
||||
if (!c.getChosenType2().isEmpty()) {
|
||||
newText.append("|ChosenType2:").append(c.getChosenType2());
|
||||
}
|
||||
|
||||
if (!c.getNamedCard().isEmpty()) {
|
||||
newText.append("|NamedCard:").append(c.getNamedCard());
|
||||
}
|
||||
@@ -638,7 +624,8 @@ public abstract class GameState {
|
||||
markedDamage.clear();
|
||||
cardToChosenClrs.clear();
|
||||
cardToChosenCards.clear();
|
||||
cardToChosenTypes.clear();
|
||||
cardToChosenType.clear();
|
||||
cardToChosenType2.clear();
|
||||
cardToMergedCards.clear();
|
||||
cardToScript.clear();
|
||||
cardAttackMap.clear();
|
||||
@@ -733,7 +720,7 @@ public abstract class GameState {
|
||||
if (persistent) {
|
||||
produced.put("PersistentMana", "True");
|
||||
}
|
||||
final AbilityManaPart abMana = new AbilityManaPart(dummy, null, produced);
|
||||
final AbilityManaPart abMana = new AbilityManaPart(dummy, produced);
|
||||
game.getAction().invoke(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -769,23 +756,10 @@ public abstract class GameState {
|
||||
}
|
||||
game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap));
|
||||
|
||||
if (!combat.getAttackers().isEmpty()) {
|
||||
List<GameEntity> attackedTarget = Lists.newArrayList();
|
||||
for (final Card c : combat.getAttackers()) {
|
||||
attackedTarget.add(combat.getDefenderByAttacker(c));
|
||||
}
|
||||
final Map<AbilityKey, Object> runParams = Maps.newEnumMap(AbilityKey.class);
|
||||
runParams.put(AbilityKey.Attackers, combat.getAttackers());
|
||||
runParams.put(AbilityKey.AttackingPlayer, combat.getAttackingPlayer());
|
||||
runParams.put(AbilityKey.AttackedTarget, attackedTarget);
|
||||
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
|
||||
}
|
||||
|
||||
for (final Card c : combat.getAttackers()) {
|
||||
CombatUtil.checkDeclaredAttacker(game, c, combat);
|
||||
CombatUtil.checkDeclaredAttacker(game, c, combat, false);
|
||||
}
|
||||
|
||||
game.getTriggerHandler().resetActiveTriggers();
|
||||
game.updateCombatForView();
|
||||
game.fireEvent(new GameEventCombatChanged());
|
||||
|
||||
@@ -828,7 +802,6 @@ public abstract class GameState {
|
||||
Card exiledWith = idToCard.get(Integer.parseInt(id));
|
||||
c.setExiledWith(exiledWith);
|
||||
c.setExiledBy(exiledWith.getController());
|
||||
exiledWith.addExiledWith(c);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1087,13 +1060,19 @@ public abstract class GameState {
|
||||
Card c = entry.getKey();
|
||||
List<String> colors = entry.getValue();
|
||||
|
||||
c.setChosenColors(colors, c.getFirstSpellAbility());
|
||||
c.setChosenColors(colors);
|
||||
}
|
||||
|
||||
// Chosen type
|
||||
for (Entry<Card, List<String>> entry : cardToChosenTypes.entrySet()) {
|
||||
for (Entry<Card, String> entry : cardToChosenType.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
c.setChosenType(entry.getValue(), c.getFirstSpellAbility());
|
||||
c.setChosenType(entry.getValue());
|
||||
}
|
||||
|
||||
// Chosen type 2
|
||||
for (Entry<Card, String> entry : cardToChosenType2.entrySet()) {
|
||||
Card c = entry.getKey();
|
||||
c.setChosenType2(entry.getValue());
|
||||
}
|
||||
|
||||
// Named card
|
||||
@@ -1196,7 +1175,7 @@ public abstract class GameState {
|
||||
String[] allCounterStrings = counterString.split(",");
|
||||
for (final String counterPair : allCounterStrings) {
|
||||
String[] pair = counterPair.split("=", 2);
|
||||
entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, false, false, null);
|
||||
entity.addCounter(CounterType.getType(pair[0]), Integer.parseInt(pair[1]), null, null, false, false, null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1236,8 +1215,7 @@ public abstract class GameState {
|
||||
boolean tapped = c.isTapped();
|
||||
boolean sickness = c.hasSickness();
|
||||
Map<CounterType, Integer> counters = c.getCounters();
|
||||
// Note: Not clearCounters() since we want to keep the counters
|
||||
// var as-is.
|
||||
// Note: Not clearCounters() since we want to keep the counters var as-is.
|
||||
c.setCounters(Maps.newHashMap());
|
||||
if (c.isAura()) {
|
||||
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
|
||||
@@ -1340,7 +1318,7 @@ public abstract class GameState {
|
||||
}
|
||||
else if (info.startsWith("OnAdventure")) {
|
||||
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ExileOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.nonCopiedSpell";
|
||||
AbilitySub saAdventure = (AbilitySub)AbilityFactory.getAbility(abAdventure, c);
|
||||
SpellAbility saAdventure = AbilityFactory.getAbility(abAdventure, c);
|
||||
StringBuilder sbPlay = new StringBuilder();
|
||||
sbPlay.append("Mode$ Continuous | MayPlay$ True | EffectZone$ Command | Affected$ Card.IsRemembered+nonAdventure");
|
||||
sbPlay.append(" | AffectedZone$ Exile | Description$ You may cast the card.");
|
||||
@@ -1379,7 +1357,9 @@ public abstract class GameState {
|
||||
} else if (info.startsWith("ChosenColor:")) {
|
||||
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
} else if (info.startsWith("ChosenType:")) {
|
||||
cardToChosenTypes.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
|
||||
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("ChosenType2:")) {
|
||||
cardToChosenType2.put(c, info.substring(info.indexOf(':') + 1));
|
||||
} else if (info.startsWith("ChosenCards:")) {
|
||||
CardCollection chosen = new CardCollection();
|
||||
String[] idlist = info.substring(info.indexOf(':') + 1).split(",");
|
||||
|
||||
@@ -8,6 +8,7 @@ import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.ImmutablePair;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
@@ -79,9 +80,10 @@ import forge.util.MyRandom;
|
||||
import forge.util.collect.FCollection;
|
||||
import forge.util.collect.FCollectionView;
|
||||
|
||||
/**
|
||||
|
||||
/**
|
||||
* A prototype for player controller class
|
||||
*
|
||||
*
|
||||
* Handles phase skips for now.
|
||||
*/
|
||||
public class PlayerControllerAi extends PlayerController {
|
||||
@@ -92,11 +94,11 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
brains = new AiController(p, game);
|
||||
}
|
||||
|
||||
|
||||
public void allowCheatShuffle(boolean value){
|
||||
brains.allowCheatShuffle(value);
|
||||
}
|
||||
|
||||
|
||||
public void setUseSimulation(boolean value) {
|
||||
brains.setUseSimulation(value);
|
||||
}
|
||||
@@ -129,6 +131,12 @@ public class PlayerControllerAi extends PlayerController {
|
||||
return ComputerUtilCombat.distributeAIDamage(attacker, blockers, damageDealt, defender, overrideOrder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<GameEntity, Integer> divideShield(Card effectSource, Map<GameEntity, Integer> affected, int shieldAmount) {
|
||||
// TODO: AI current can't use this so this is not implemented.
|
||||
return new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer announceRequirements(SpellAbility ability, String announce) {
|
||||
// For now, these "announcements" are made within the AI classes of the appropriate SA effects
|
||||
@@ -155,7 +163,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
case BidLife:
|
||||
return 0;
|
||||
default:
|
||||
return null;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null; // return incorrect value to indicate that
|
||||
@@ -238,7 +246,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
public boolean confirmAction(SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return getAi().confirmAction(sa, mode, message);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean confirmBidAction(SpellAbility sa, PlayerActionConfirmMode mode, String string,
|
||||
int bid, Player winner) {
|
||||
@@ -528,7 +536,14 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public CardCollectionView chooseCardsToDiscardUnlessType(int num, CardCollectionView hand, String uType, SpellAbility sa) {
|
||||
final CardCollectionView cardsOfType = CardLists.getType(hand, uType);
|
||||
String [] splitUTypes = uType.split(",");
|
||||
CardCollection cardsOfType = new CardCollection();
|
||||
for (String part : splitUTypes) {
|
||||
CardCollection partCards = CardLists.getType(hand, part);
|
||||
if (!partCards.isEmpty()) {
|
||||
cardsOfType.addAll(partCards);
|
||||
}
|
||||
}
|
||||
if (!cardsOfType.isEmpty()) {
|
||||
Card toDiscard = Aggregates.itemWithMin(cardsOfType, CardPredicates.Accessors.fnGetCmc);
|
||||
return new CardCollection(toDiscard);
|
||||
@@ -543,12 +558,13 @@ public class PlayerControllerAi extends PlayerController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> chooseSomeType(String kindOfType, SpellAbility sa, int min, int max, List<String> validTypes) {
|
||||
List<String> chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), min, max, validTypes);
|
||||
if (chosen.isEmpty()) {
|
||||
return validTypes.subList(0, min);
|
||||
public String chooseSomeType(String kindOfType, SpellAbility sa, Collection<String> validTypes, List<String> invalidTypes, boolean isOptional) {
|
||||
String chosen = ComputerUtil.chooseSomeType(player, kindOfType, sa.getParam("AILogic"), validTypes, invalidTypes);
|
||||
if (StringUtils.isBlank(chosen) && !validTypes.isEmpty()) {
|
||||
chosen = validTypes.iterator().next();
|
||||
System.err.println("AI has no idea how to choose " + kindOfType +", defaulting to arbitrary element: chosen");
|
||||
}
|
||||
getGame().getAction().notifyOfValue(sa, player, chosen.toString(), player);
|
||||
getGame().getAction().notifyOfValue(sa, player, chosen, player);
|
||||
return chosen;
|
||||
}
|
||||
|
||||
@@ -558,8 +574,8 @@ public class PlayerControllerAi extends PlayerController {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, String question) {
|
||||
return brains.aiShouldRun(replacementEffect, effectSA);
|
||||
public boolean confirmReplacementEffect(ReplacementEffect replacementEffect, SpellAbility effectSA, GameEntity affected, String question) {
|
||||
return brains.aiShouldRun(replacementEffect, effectSA, affected);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -636,10 +652,9 @@ public class PlayerControllerAi extends PlayerController {
|
||||
public List<SpellAbility> chooseSpellAbilityToPlay() {
|
||||
return brains.chooseSpellAbilityToPlay();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean playChosenSpellAbility(SpellAbility sa) {
|
||||
// System.out.println("Playing sa: " + sa);
|
||||
if (sa instanceof LandAbility) {
|
||||
if (sa.canPlay()) {
|
||||
sa.resolve();
|
||||
@@ -649,7 +664,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
ComputerUtil.handlePlayingSpellAbility(player, sa, getGame());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CardCollection chooseCardsToDiscardToMaximumHandSize(int numDiscard) {
|
||||
@@ -702,7 +717,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
public int chooseNumber(SpellAbility sa, String title, int min, int max) {
|
||||
return brains.chooseNumber(sa, title, min, max);
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public int chooseNumber(SpellAbility sa, String string, int min, int max, Map<String, Object> params) {
|
||||
ApiType api = sa.getApi();
|
||||
@@ -778,7 +793,8 @@ public class PlayerControllerAi extends PlayerController {
|
||||
case "BetterTgtThanRemembered":
|
||||
if (source.getRememberedCount() > 0) {
|
||||
Card rem = (Card) source.getFirstRemembered();
|
||||
if (!rem.isInZone(ZoneType.Battlefield)) {
|
||||
// avoid pumping opponent creature
|
||||
if (!rem.isInZone(ZoneType.Battlefield) || rem.getController().isOpponentOf(source.getController())) {
|
||||
return true;
|
||||
}
|
||||
for (Card c : source.getController().getCreaturesInPlay()) {
|
||||
@@ -804,7 +820,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
*
|
||||
* @see
|
||||
* forge.game.player.PlayerController#chooseBinary(forge.game.spellability.
|
||||
* SpellAbility, java.lang.String,
|
||||
@@ -845,7 +861,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
}
|
||||
return sa.getChosenList();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public byte chooseColorAllowColorless(String message, Card card, ColorSet colors) {
|
||||
final String c = ComputerUtilCard.getMostProminentColor(player.getCardsIn(ZoneType.Hand));
|
||||
@@ -891,7 +907,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
*
|
||||
* @see forge.game.player.PlayerController#chooseCounterType(java.util.List,
|
||||
* forge.game.spellability.SpellAbility, java.lang.String, java.util.Map)
|
||||
*/
|
||||
@@ -907,7 +923,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
|
||||
@Override
|
||||
public boolean confirmPayment(CostPart costPart, String prompt, SpellAbility sa) {
|
||||
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
|
||||
return brains.confirmPayment(costPart); // AI is expected to know what it is paying for at the moment (otherwise add another parameter to this method)
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -988,6 +1004,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
emptyAbility.setActivatingPlayer(player);
|
||||
emptyAbility.setTriggeringObjects(sa.getTriggeringObjects());
|
||||
emptyAbility.setSVars(sa.getSVars());
|
||||
emptyAbility.setXManaCostPaid(sa.getRootAbility().getXManaCostPaid());
|
||||
if (ComputerUtilCost.willPayUnlessCost(sa, player, cost, alreadyPaid, allPayers) && ComputerUtilCost.canPayCost(emptyAbility, player)) {
|
||||
ComputerUtil.playNoStack(player, emptyAbility, getGame()); // AI needs something to resolve to pay that cost
|
||||
return true;
|
||||
@@ -1019,7 +1036,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
*/
|
||||
if (sa.isMayChooseNewTargets() && !sa.setupTargets()) {
|
||||
if (sa.isSpell()) {
|
||||
sa.getHostCard().ceaseToExist();
|
||||
player.getGame().getAction().ceaseToExist(sa.getHostCard(), false);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -1053,14 +1070,13 @@ public class PlayerControllerAi extends PlayerController {
|
||||
boolean noManaCost = tgtSA.hasParam("WithoutManaCost");
|
||||
if (tgtSA instanceof Spell) { // Isn't it ALWAYS a spell?
|
||||
Spell spell = (Spell) tgtSA;
|
||||
if (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional) {
|
||||
if (tgtSA.checkRestrictions(brains.getPlayer()) && (brains.canPlayFromEffectAI(spell, !optional, noManaCost) == AiPlayDecision.WillPlay || !optional)) {
|
||||
if (noManaCost) {
|
||||
return ComputerUtil.playSpellAbilityWithoutPayingManaCost(player, tgtSA, getGame());
|
||||
} else {
|
||||
return ComputerUtil.playStack(tgtSA, player, getGame());
|
||||
}
|
||||
} else
|
||||
return false; // didn't play spell
|
||||
return ComputerUtil.playStack(tgtSA, player, getGame());
|
||||
}
|
||||
return false; // didn't play spell
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1083,7 +1099,6 @@ public class PlayerControllerAi extends PlayerController {
|
||||
boolean allCreatures = Iterables.all(Iterables.concat(pile1, pile2), CardPredicates.Presets.CREATURES);
|
||||
int cmc1 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile1) : ComputerUtilCard.evaluatePermanentList(pile1);
|
||||
int cmc2 = allCreatures ? ComputerUtilCard.evaluateCreatureList(pile2) : ComputerUtilCard.evaluatePermanentList(pile2);
|
||||
System.out.println("value:" + cmc1 + " " + cmc2);
|
||||
|
||||
// for now, this assumes that the outcome will be bad
|
||||
// TODO: This should really have a ChooseLogic param to
|
||||
@@ -1096,7 +1111,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
public void revealAnte(String message, Multimap<Player, PaperCard> removedAnteCards) {
|
||||
// Ai won't understand that anyway
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Collection<? extends PaperCard> complainCardsCantPlayWell(Deck myDeck) {
|
||||
return brains.complainCardsCantPlayWell(myDeck);
|
||||
@@ -1144,7 +1159,7 @@ public class PlayerControllerAi extends PlayerController {
|
||||
return new HashMap<>();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
//Do not convoke potential blockers until after opponent's attack
|
||||
final CardCollectionView blockers = ComputerUtilCard.getLikelyBlockers(ai, null);
|
||||
if ((ph.isPlayerTurn(ai) && ph.getPhase().isAfter(PhaseType.COMBAT_BEGIN)) ||
|
||||
|
||||
@@ -39,7 +39,6 @@ import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CardUtil;
|
||||
@@ -112,6 +111,63 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Brain in a Jar
|
||||
public static class BrainInAJar {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
int counterNum = source.getCounters(CounterEnumType.CHARGE);
|
||||
// no need for logic
|
||||
if (counterNum == 0) {
|
||||
return false;
|
||||
}
|
||||
int libsize = ai.getCardsIn(ZoneType.Library).size();
|
||||
|
||||
final CardCollection hand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.or(
|
||||
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
|
||||
if (!hand.isEmpty()) {
|
||||
// has spell that can be cast in hand with put ability
|
||||
if (!CardLists.filter(hand, CardPredicates.hasCMC(counterNum + 1)).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// has spell that can be cast if one counter is removed
|
||||
if (!CardLists.filter(hand, CardPredicates.hasCMC(counterNum)).isEmpty()) {
|
||||
sa.setXManaCostPaid(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
final CardCollection library = CardLists.filter(ai.getCardsIn(ZoneType.Library), Predicates.or(
|
||||
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
|
||||
if (!library.isEmpty()) {
|
||||
// get max cmc of instant or sorceries in the libary
|
||||
int maxCMC = 0;
|
||||
for (final Card c : library) {
|
||||
int v = c.getCMC();
|
||||
if (c.isSplitCard()) {
|
||||
v = Math.max(c.getCMC(Card.SplitCMCMode.LeftSplitCMC), c.getCMC(Card.SplitCMCMode.RightSplitCMC));
|
||||
}
|
||||
if (v > maxCMC) {
|
||||
maxCMC = v;
|
||||
}
|
||||
}
|
||||
// there is a spell with more CMC, no need to remove counter
|
||||
if (counterNum + 1 < maxCMC) {
|
||||
return false;
|
||||
}
|
||||
int maxToRemove = counterNum - maxCMC + 1;
|
||||
// no Scry 0, even if its catched from later stuff
|
||||
if (maxToRemove <= 0) {
|
||||
return false;
|
||||
}
|
||||
sa.setXManaCostPaid(maxToRemove);
|
||||
} else {
|
||||
// no Instant or Sorceries anymore, just scry
|
||||
sa.setXManaCostPaid(Math.min(counterNum, libsize));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Chain of Acid
|
||||
public static class ChainOfAcid {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
@@ -159,10 +215,9 @@ public class SpecialCardAi {
|
||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
final Combat combat = ai.getGame().getCombat();
|
||||
|
||||
Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa);
|
||||
animated.addType("Creature");
|
||||
Card animated = AnimateAi.becomeAnimated(sa.getHostCard(), sa.getSubAbility());
|
||||
if (sa.getHostCard().canReceiveCounters(CounterEnumType.P1P1)) {
|
||||
animated.addCounter(CounterEnumType.P1P1, 2, ai, false, null);
|
||||
animated.addCounter(CounterEnumType.P1P1, 2, ai, sa.getSubAbility(), false, null);
|
||||
}
|
||||
boolean isOppEOT = ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
|
||||
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
|
||||
@@ -170,10 +225,6 @@ public class SpecialCardAi {
|
||||
|
||||
return isOppEOT || isValuableAttacker || isValuableBlocker;
|
||||
}
|
||||
|
||||
public static SpellAbility considerAnimating(final Player ai, final SpellAbility sa, final List<SpellAbility> options) {
|
||||
return ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) ? options.get(0) : options.get(1);
|
||||
}
|
||||
}
|
||||
|
||||
// Cursed Scroll
|
||||
@@ -327,7 +378,7 @@ public class SpecialCardAi {
|
||||
}
|
||||
|
||||
// Do not activate if damage will be prevented
|
||||
if (source.staticDamagePrevention(predictedPT.getLeft(), source, true, true) == 0) {
|
||||
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -911,6 +962,39 @@ public class SpecialCardAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Multiple Choice
|
||||
public static class MultipleChoice {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
int maxX = ComputerUtilCost.getMaxXValue(sa, ai);
|
||||
|
||||
if (maxX == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean canScryDraw = maxX >= 1 && ai.getCardsIn(ZoneType.Library).size() >= 3; // TODO: generalize / use profile values
|
||||
boolean canBounce = maxX >= 2 && !ai.getOpponents().getCreaturesInPlay().isEmpty();
|
||||
boolean shouldBounce = canBounce && ComputerUtilCard.evaluateCreature(ComputerUtilCard.getWorstCreatureAI(ai.getOpponents().getCreaturesInPlay())) > 210; // 180 is the level of a 4/4 token creature
|
||||
boolean canMakeToken = maxX >= 3;
|
||||
boolean canDoAll = maxX >= 4 && canScryDraw && shouldBounce;
|
||||
|
||||
if (canDoAll) {
|
||||
sa.setXManaCostPaid(4);
|
||||
return true;
|
||||
} else if (canMakeToken) {
|
||||
sa.setXManaCostPaid(3);
|
||||
return true;
|
||||
} else if (shouldBounce) {
|
||||
sa.setXManaCostPaid(2);
|
||||
return true;
|
||||
} else if (canScryDraw) {
|
||||
sa.setXManaCostPaid(1);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Necropotence
|
||||
public static class Necropotence {
|
||||
public static boolean consider(final Player ai, final SpellAbility sa) {
|
||||
|
||||
@@ -58,6 +58,11 @@ public abstract class SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final Cost cost = sa.getPayCosts();
|
||||
|
||||
if (sa.hasParam("AICheckCanPlayWithDefinedX")) {
|
||||
// FIXME: can this somehow be simplified without the need for an extra AI hint?
|
||||
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai));
|
||||
}
|
||||
|
||||
if (!checkConditions(ai, sa, sa.getConditions())) {
|
||||
SpellAbility sub = sa.getSubAbility();
|
||||
if (sub != null && !checkConditions(ai, sub, sub.getConditions())) {
|
||||
@@ -157,7 +162,7 @@ public abstract class SpellAbilityAi {
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false; // prevent infinite loop
|
||||
}
|
||||
return MyRandom.getRandom().nextFloat() < .8f; // random success
|
||||
return MyRandom.getRandom().nextFloat() < .8f; // random success
|
||||
}
|
||||
|
||||
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
@@ -188,7 +193,7 @@ public abstract class SpellAbilityAi {
|
||||
* Handles the AI decision to play a triggered SpellAbility
|
||||
*/
|
||||
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
|
||||
if (canPlayWithoutRestrict(aiPlayer, sa)) {
|
||||
if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -299,7 +304,7 @@ public abstract class SpellAbilityAi {
|
||||
final AbilitySub subAb = ab.getSubAbility();
|
||||
return SpellApiToAi.Converter.get(ab.getApi()).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
|
||||
}
|
||||
|
||||
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
System.err.println("Warning: default (ie. inherited from base class) implementation of confirmAction is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
|
||||
return true;
|
||||
|
||||
@@ -34,6 +34,7 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.BidLife, BidLifeAi.class)
|
||||
.put(ApiType.Bond, BondAi.class)
|
||||
.put(ApiType.Branch, AlwaysPlayAi.class)
|
||||
.put(ApiType.Camouflage, ChooseCardAi.class)
|
||||
.put(ApiType.ChangeCombatants, ChangeCombatantsAi.class)
|
||||
.put(ApiType.ChangeTargets, ChangeTargetsAi.class)
|
||||
.put(ApiType.ChangeX, AlwaysPlayAi.class)
|
||||
@@ -94,8 +95,10 @@ public enum SpellApiToAi {
|
||||
.put(ApiType.Haunt, HauntAi.class)
|
||||
.put(ApiType.ImmediateTrigger, ImmediateTriggerAi.class)
|
||||
.put(ApiType.Investigate, InvestigateAi.class)
|
||||
.put(ApiType.Learn, LearnAi.class)
|
||||
.put(ApiType.LoseLife, LifeLoseAi.class)
|
||||
.put(ApiType.LosesGame, GameLossAi.class)
|
||||
.put(ApiType.MakeCard, AlwaysPlayAi.class)
|
||||
.put(ApiType.Mana, ManaEffectAi.class)
|
||||
.put(ApiType.ManaReflected, CannotPlayAi.class)
|
||||
.put(ApiType.Manifest, ManifestAi.class)
|
||||
|
||||
@@ -21,8 +21,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
final Player opp = ai.getStrongestOpponent();
|
||||
|
||||
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
|
||||
if (list.isEmpty()) {
|
||||
@@ -40,12 +39,13 @@ public class ActivateAbilityAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = ai.getStrongestOpponent();
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
@@ -30,6 +30,7 @@ import forge.game.cost.CostPutCounter;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbility;
|
||||
import forge.game.staticability.StaticAbilityContinuous;
|
||||
@@ -99,30 +100,30 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
// Don't use instant speed animate abilities before AI's COMBAT_BEGIN
|
||||
if (!ph.is(PhaseType.COMBAT_BEGIN) && ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
|
||||
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
|
||||
&& !sa.hasParam("ActivationPhases") && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
return false;
|
||||
}
|
||||
// Don't use instant speed animate abilities outside human's
|
||||
// COMBAT_DECLARE_ATTACKERS or if no attackers
|
||||
if (ph.getPlayerTurn().isOpponentOf(ai) && !sa.hasParam("Permanent")
|
||||
if (ph.getPlayerTurn().isOpponentOf(ai) && !"Permanent".equals(sa.getParam("Duration"))
|
||||
&& (!ph.is(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
|| game.getCombat() != null && game.getCombat().getAttackersOf(ai).isEmpty())) {
|
||||
return false;
|
||||
}
|
||||
// Don't activate during MAIN2 unless this effect is permanent
|
||||
if (ph.is(PhaseType.MAIN2) && !sa.hasParam("Permanent") && !sa.hasParam("UntilYourNextTurn")) {
|
||||
if (ph.is(PhaseType.MAIN2) && !"Permanent".equals(sa.getParam("Duration")) && !"UntilYourNextTurn".equals(sa.getParam("Duration"))) {
|
||||
return false;
|
||||
}
|
||||
// Don't animate if the AI won't attack anyway or use as a potential blocker
|
||||
Player opponent = ai.getWeakestOpponent();
|
||||
// Activating as a potential blocker is only viable if it's an ability activated from a permanent, otherwise
|
||||
// the AI will waste resources
|
||||
boolean activateAsPotentialBlocker = sa.hasParam("UntilYourNextTurn")
|
||||
boolean activateAsPotentialBlocker = "UntilYourNextTurn".equals(sa.getParam("Duration"))
|
||||
&& ai.getGame().getPhaseHandler().getNextTurn() != ai
|
||||
&& source.isPermanent();
|
||||
if (ph.isPlayerTurn(ai) && ai.getLife() < 6 && opponent.getLife() > 6
|
||||
&& Iterables.any(opponent.getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES)
|
||||
&& !sa.hasParam("AILogic") && !sa.hasParam("Permanent") && !activateAsPotentialBlocker) {
|
||||
&& !sa.hasParam("AILogic") && !"Permanent".equals(sa.getParam("Duration")) && !activateAsPotentialBlocker) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
@@ -155,7 +156,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
&& !c.isEquipping();
|
||||
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (!bFlag && c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
|
||||
if (!bFlag && c.isCreature() && ("Permanent".equals(sa.getParam("Duration")) || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(c, sa.getParam("Power"), sa);
|
||||
@@ -178,7 +179,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (!SpellAbilityAi.isSorcerySpeed(sa) && !sa.hasParam("Permanent")) {
|
||||
if (!SpellAbilityAi.isSorcerySpeed(sa) && !"Permanent".equals(sa.getParam("Duration"))) {
|
||||
if (sa.hasParam("Crew") && c.isCreature()) {
|
||||
// Do not try to crew a vehicle which is already a creature
|
||||
return false;
|
||||
@@ -243,6 +244,11 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2);
|
||||
}
|
||||
|
||||
private boolean animateTgtAI(final SpellAbility sa) {
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
@@ -272,7 +278,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
Map<Card, Integer> data = Maps.newHashMap();
|
||||
for (final Card c : list) {
|
||||
// don't use Permanent animate on something that would leave the field
|
||||
if (c.hasSVar("EndOfTurnLeavePlay") && sa.hasParam("Permanent")) {
|
||||
if (c.hasSVar("EndOfTurnLeavePlay") && "Permanent".equals(sa.getParam("Duration"))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -306,9 +312,9 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
// if its player turn,
|
||||
// check if its Permanent or that creature would attack
|
||||
if (ph.isPlayerTurn(ai)) {
|
||||
if (!sa.hasParam("Permanent")
|
||||
if (!"Permanent".equals(sa.getParam("Duration"))
|
||||
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animatedCopy)
|
||||
&& !sa.hasParam("UntilHostLeavesPlay")) {
|
||||
&& !"UntilHostLeavesPlay".equals(sa.getParam("Duration"))) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -354,8 +360,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
|
||||
// two are the only things
|
||||
// that animate a target. Those can just use AI:RemoveDeck:All until
|
||||
// this can do a reasonably
|
||||
// good job of picking a good target
|
||||
// this can do a reasonably good job of picking a good target
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -401,7 +406,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
// allow ChosenType - overrides anything else specified
|
||||
if (types.hasSubtype("ChosenType")) {
|
||||
types.clear();
|
||||
types.addAll(sa.getChosenType());
|
||||
types.add(source.getChosenType());
|
||||
}
|
||||
|
||||
final List<String> keywords = Lists.newArrayList();
|
||||
@@ -432,7 +437,7 @@ public class AnimateAi extends SpellAbilityAi {
|
||||
if (sa.hasParam("Colors")) {
|
||||
final String colors = sa.getParam("Colors");
|
||||
if (colors.equals("ChosenColor")) {
|
||||
tmpDesc = CardUtil.getShortColorsString(sa.getChosenColors());
|
||||
tmpDesc = CardUtil.getShortColorsString(source.getChosenColors());
|
||||
} else {
|
||||
tmpDesc = CardUtil.getShortColorsString(Lists.newArrayList(Arrays.asList(colors.split(","))));
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Lists;
|
||||
@@ -554,12 +555,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
evenBetterList = CardLists.filter(betterList, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.isUntapped();
|
||||
}
|
||||
});
|
||||
evenBetterList = CardLists.filter(betterList, CardPredicates.Presets.UNTAPPED);
|
||||
if (!evenBetterList.isEmpty()) {
|
||||
betterList = evenBetterList;
|
||||
}
|
||||
@@ -733,17 +729,15 @@ public class AttachAi extends SpellAbilityAi {
|
||||
*/
|
||||
private static Card attachAISpecificCardPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory,
|
||||
final Card attachSource) {
|
||||
// I know this isn't much better than Hardcoding, but some cards need it for now
|
||||
final Player ai = sa.getActivatingPlayer();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
Card chosen = null;
|
||||
|
||||
if ("Guilty Conscience".equals(sourceName)) {
|
||||
chosen = SpecialCardAi.GuiltyConscience.getBestAttachTarget(ai, sa, list);
|
||||
} else if ("Bonds of Faith".equals(sourceName)) {
|
||||
chosen = doPumpOrCurseAILogic(ai, sa, list, "Human");
|
||||
} else if ("Clutch of Undeath".equals(sourceName)) {
|
||||
chosen = doPumpOrCurseAILogic(ai, sa, list, "Zombie");
|
||||
} else if (sa.hasParam("AIValid")) {
|
||||
// TODO: Make the AI recognize which cards to pump based on the card's abilities alone
|
||||
chosen = doPumpOrCurseAILogic(ai, sa, list, sa.getParam("AIValid"));
|
||||
}
|
||||
|
||||
// If Mandatory (brought directly into play without casting) gotta
|
||||
@@ -767,7 +761,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
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);
|
||||
powerBuff = AbilityUtils.calculateAmount(sa.getHostCard(), stAb.getParam("AddPower"), stAb);
|
||||
}
|
||||
}
|
||||
if (combat != null && combat.isAttacking(equipped) && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS, sa.getActivatingPlayer())) {
|
||||
@@ -1129,8 +1123,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
for (Card target : list) {
|
||||
for (Trigger t : target.getTriggers()) {
|
||||
if (t.getMode() == TriggerType.SpellCast) {
|
||||
final Map<String, String> params = t.getMapParams();
|
||||
if ("Card.Self".equals(params.get("TargetsValid")) && "You".equals(params.get("ValidActivatingPlayer"))) {
|
||||
if ("Card.Self".equals(t.getParam("TargetsValid")) && "You".equals(t.getParam("ValidActivatingPlayer"))) {
|
||||
magnetList.add(target);
|
||||
break;
|
||||
}
|
||||
@@ -1646,7 +1639,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
return ComputerUtilCombat.canAttackNextTurn(card) && card.getNetCombatDamage() >= 1;
|
||||
} else if (keyword.endsWith("CARDNAME attacks each turn if able.") || keyword.endsWith("CARDNAME attacks each combat if able.")) {
|
||||
return ComputerUtilCombat.canAttackNextTurn(card) && CombatUtil.canBlock(card, true) && !ai.getCreaturesInPlay().isEmpty();
|
||||
} else if (keyword.endsWith("CARDNAME can't block.") || keyword.contains("CantBlock")) {
|
||||
} else if (keyword.endsWith("CARDNAME can't block.")) {
|
||||
return CombatUtil.canBlock(card, true);
|
||||
} else if (keyword.endsWith("CARDNAME's activated abilities can't be activated.")) {
|
||||
for (SpellAbility ability : card.getSpellAbilities()) {
|
||||
@@ -1699,7 +1692,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
if (!c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return c.getType().hasCreatureType(type);
|
||||
return c.isValid(type, ai, sa.getHostCard(), sa);
|
||||
}
|
||||
});
|
||||
List<Card> oppNonType = CardLists.filter(list, new Predicate<Card>() {
|
||||
@@ -1709,7 +1702,7 @@ public class AttachAi extends SpellAbilityAi {
|
||||
if (c.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
return !c.getType().hasCreatureType(type) && !ComputerUtilCard.isUselessCreature(ai, c);
|
||||
return !c.isValid(type, ai, sa.getHostCard(), sa) && !ComputerUtilCard.isUselessCreature(ai, c);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -13,18 +13,21 @@ public class BalanceAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
|
||||
int diff = 0;
|
||||
// TODO Add support for multiplayer logic
|
||||
final Player opp = aiPlayer.getWeakestOpponent();
|
||||
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
|
||||
Player opp = aiPlayer.getWeakestOpponent();
|
||||
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);
|
||||
for (Player min : aiPlayer.getOpponents()) {
|
||||
if (min.getCardsIn(ZoneType.Battlefield).size() < opp.getCardsIn(ZoneType.Battlefield).size()) {
|
||||
opp = min;
|
||||
}
|
||||
}
|
||||
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
|
||||
|
||||
if ("BalanceCreaturesAndLands".equals(logic)) {
|
||||
// Copied over from hardcoded Balance. We should be checking value of the lands/creatures not just counting
|
||||
// TODO Copied over from hardcoded Balance. We should be checking value of the lands/creatures for each opponent, not just counting
|
||||
diff += CardLists.filter(humPerms, CardPredicates.Presets.LANDS).size() -
|
||||
CardLists.filter(compPerms, CardPredicates.Presets.LANDS).size();
|
||||
diff += 1.5 * ( CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() -
|
||||
diff += 1.5 * (CardLists.filter(humPerms, CardPredicates.Presets.CREATURES).size() -
|
||||
CardLists.filter(compPerms, CardPredicates.Presets.CREATURES).size());
|
||||
}
|
||||
else if ("BalancePermanents".equals(logic)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi {
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canTgtCreature()) {
|
||||
List<Card> list = CardLists.getTargetableCards(aiPlayer.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), sa);
|
||||
List<Card> list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
|
||||
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
|
||||
if (list.isEmpty()) {
|
||||
return false;
|
||||
|
||||
@@ -10,6 +10,7 @@ import forge.game.card.Card;
|
||||
import forge.game.mana.ManaCostBeingPaid;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetChoices;
|
||||
|
||||
public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
|
||||
@@ -40,18 +41,20 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
// nothing on stack, so nothing to target
|
||||
return false;
|
||||
}
|
||||
final TargetChoices topTargets = topSa.getTargets();
|
||||
final Card topHost = topSa.getHostCard();
|
||||
|
||||
if (sa.getTargets().size() != 0) {
|
||||
if (sa.getTargets().size() != 0 && sa.isTrigger()) {
|
||||
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!topSa.usesTargeting() || topSa.getTargets().getTargetCards().contains(sa.getHostCard())) {
|
||||
if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) {
|
||||
// if this does not target at all or already targets host, no need to redirect it again
|
||||
return false;
|
||||
}
|
||||
|
||||
for (Card tgt : topSa.getTargets().getTargetCards()) {
|
||||
for (Card tgt : topTargets.getTargetCards()) {
|
||||
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) {
|
||||
// We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites),
|
||||
// no need to retarget again to another one
|
||||
@@ -59,7 +62,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
if (topSa.getHostCard() != null && !topSa.getHostCard().getController().isOpponentOf(aiPlayer)) {
|
||||
if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) {
|
||||
// make sure not to redirect our own abilities
|
||||
return false;
|
||||
}
|
||||
@@ -71,7 +74,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
// don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face)
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) {
|
||||
ManaCost manaCost = sa.getPayCosts().getCostMana().getMana();
|
||||
int payDamage = manaCost.getPhyrexianCount() * 2;
|
||||
@@ -80,12 +83,22 @@ public class ChangeTargetsAi extends SpellAbilityAi {
|
||||
ManaCost normalizedMana = manaCost.getNormalizedMana();
|
||||
boolean canPay = ComputerUtilMana.canPayManaCost(new ManaCostBeingPaid(normalizedMana), sa, aiPlayer);
|
||||
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
|
||||
&& topSa.getTargets().contains(aiPlayer)) {
|
||||
&& topTargets.contains(aiPlayer)) {
|
||||
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Card firstCard = topTargets.getFirstTargetedCard();
|
||||
// if we're not the target don't intervene unless we can steal a buff
|
||||
if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) {
|
||||
return false;
|
||||
}
|
||||
Player firstPlayer = topTargets.getFirstTargetedPlayer();
|
||||
if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(topSa);
|
||||
return true;
|
||||
|
||||
@@ -15,6 +15,7 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Maps;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiBlockController;
|
||||
import forge.ai.AiCardMemory;
|
||||
import forge.ai.AiController;
|
||||
@@ -57,6 +58,7 @@ import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
@@ -74,11 +76,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
|
||||
Card host = sa.getHostCard();
|
||||
|
||||
if (host != null && host.hasSVar("AIPreferenceOverride")) {
|
||||
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
|
||||
// currently used by SacAndUpgrade logic, might need simplification
|
||||
host.removeSVar("AIPreferenceOverride");
|
||||
sa.getHostCard().removeSVar("AIPreferenceOverride");
|
||||
}
|
||||
|
||||
if (aiLogic.equals("BeforeCombat")) {
|
||||
@@ -92,7 +92,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
} 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(host.getName())).size() > 1;
|
||||
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());
|
||||
@@ -127,40 +127,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return true;
|
||||
} else if (aiLogic.equals("Pongify")) {
|
||||
return SpecialAiLogic.doPongifyLogic(ai, sa);
|
||||
} else if (aiLogic.equals("Ashiok")) {
|
||||
final int loyalty = host.getCurrentLoyalty() - 1;
|
||||
CardCollectionView choices = new CardCollection();
|
||||
for (int i = loyalty; i >= 0; i--) {
|
||||
sa.setXManaCostPaid(i);
|
||||
choices = ai.getGame().getCardsIn(ZoneType.listValueOf(sa.getParam("Origin")));
|
||||
choices = CardLists.getValidCards(choices, sa.getParam("ChangeType"), host.getController(), host, sa);
|
||||
if (!choices.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (aiLogic.equals("BestCard")) {
|
||||
CardCollectionView choices = ai.getGame().getCardsIn(ZoneType.listValueOf(sa.getParam("Origin")));
|
||||
choices = CardLists.getValidCards(choices, sa.getParam("ChangeType"), host.getController(), host, sa);
|
||||
if (!choices.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
} else if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
|
||||
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(host)).size();
|
||||
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
|
||||
// minimum card advantage unless the hand will be fully reloaded
|
||||
int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
|
||||
|
||||
if (numExiledWithSrc > curHandSize) {
|
||||
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(host)) {
|
||||
// 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 super.checkAiLogic(ai, sa, aiLogic);
|
||||
@@ -197,16 +163,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
|
||||
} else if (aiLogic.equals("Pongify")) {
|
||||
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
|
||||
} else if (aiLogic.equals("Ashiok")) {
|
||||
return true; // If checkAiLogic returns true, then we should be good to go
|
||||
} else if (aiLogic.equals("BestCard")) {
|
||||
return true; // If checkAiLogic returns true, then we should be good to go
|
||||
} else if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
|
||||
return true; // If checkAiLogic returns true, then we should be good to go
|
||||
}
|
||||
}
|
||||
|
||||
if (isHidden(sa)) {
|
||||
if (sa.isHidden()) {
|
||||
return hiddenOriginCanPlayAI(aiPlayer, sa);
|
||||
}
|
||||
return knownOriginCanPlayAI(aiPlayer, sa);
|
||||
@@ -223,21 +182,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
if (isHidden(sa)) {
|
||||
if (sa.isHidden()) {
|
||||
return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
|
||||
}
|
||||
return knownOriginPlayDrawbackAI(aiPlayer, sa);
|
||||
}
|
||||
|
||||
|
||||
private static boolean isHidden(SpellAbility sa) {
|
||||
boolean hidden = sa.hasParam("Hidden");
|
||||
if (!hidden && sa.hasParam("Origin")) {
|
||||
hidden = ZoneType.isHidden(sa.getParam("Origin"));
|
||||
}
|
||||
return hidden;
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
* changeZoneTriggerAINoCost.
|
||||
@@ -273,7 +223,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return delta <= 0;
|
||||
}
|
||||
|
||||
if (isHidden(sa)) {
|
||||
if (sa.isHidden()) {
|
||||
return hiddenTriggerAI(aiPlayer, sa, mandatory);
|
||||
}
|
||||
return knownOriginTriggerAI(aiPlayer, sa, mandatory);
|
||||
@@ -306,7 +256,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
ZoneType origin = null;
|
||||
final Player opponent = ai.getWeakestOpponent();
|
||||
final Player opponent = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
|
||||
|
||||
if (sa.hasParam("Origin")) {
|
||||
@@ -514,7 +464,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
// if putting cards from hand to library and parent is drawing cards
|
||||
// make sure this will actually do something:
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Player opp = aiPlayer.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
|
||||
if (tgt != null && tgt.canTgtPlayer()) {
|
||||
boolean isCurse = sa.isCurse();
|
||||
if (isCurse && sa.canTarget(opp)) {
|
||||
@@ -573,7 +523,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
Iterable<Player> pDefined;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if ((tgt != null) && tgt.canTgtPlayer()) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (sa.isCurse()) {
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
@@ -829,7 +779,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
|
||||
}
|
||||
|
||||
if (isHidden(sa)) {
|
||||
if (sa.isHidden()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -935,7 +885,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
// TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore?
|
||||
sa.setXManaCostPaid(xPay);
|
||||
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
|
||||
}
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(origin), sa);
|
||||
|
||||
@@ -956,9 +905,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
if (sa.isSpell()) {
|
||||
list.remove(source); // spells can't target their own source, because it's actually in the stack zone
|
||||
}
|
||||
//System.out.println("isPreferredTarget " + list);
|
||||
if (sa.hasParam("AttachedTo")) {
|
||||
//System.out.println("isPreferredTarget att " + list);
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
@@ -970,7 +917,6 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
//System.out.println("isPreferredTarget ok " + list);
|
||||
}
|
||||
|
||||
if (list.size() < sa.getMinTargets()) {
|
||||
@@ -1206,6 +1152,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if (!list.isEmpty()) {
|
||||
if (destination.equals(ZoneType.Battlefield) || origin.contains(ZoneType.Battlefield)) {
|
||||
// filter by MustTarget requirement
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
final Card mostExpensive = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, false);
|
||||
if (mostExpensive.isCreature()) {
|
||||
// if a creature is most expensive take the best one
|
||||
@@ -1234,6 +1184,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original list for next loop if filtered by MustTarget requirement
|
||||
if (mustTargetFiltered) {
|
||||
list = originalList;
|
||||
}
|
||||
} else if (destination.equals(ZoneType.Hand) || destination.equals(ZoneType.Library)) {
|
||||
List<Card> nonLands = CardLists.getNotType(list, "Land");
|
||||
// Prefer to pull a creature, generally more useful for AI.
|
||||
@@ -1352,8 +1307,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
private static Card canBouncePermanent(final Player ai, SpellAbility sa, CardCollectionView list) {
|
||||
Game game = ai.getGame();
|
||||
// filter out untargetables
|
||||
CardCollectionView aiPermanents = CardLists
|
||||
.filterControlledBy(list, ai);
|
||||
CardCollectionView aiPermanents = CardLists.filterControlledBy(list, ai);
|
||||
CardCollection aiPlaneswalkers = CardLists.filter(aiPermanents, Presets.PLANESWALKERS);
|
||||
|
||||
// Felidar Guardian + Saheeli Rai combo support
|
||||
@@ -1525,9 +1479,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
|
||||
if ("DeathgorgeScavenger".equals(logic)) {
|
||||
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
|
||||
} else if ("ExtraplanarLens".equals(logic)) {
|
||||
}
|
||||
if ("ExtraplanarLens".equals(logic)) {
|
||||
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
|
||||
} else if ("ExileCombatThreat".equals(logic)) {
|
||||
}
|
||||
if ("ExileCombatThreat".equals(logic)) {
|
||||
return doExileCombatThreatLogic(ai, sa);
|
||||
}
|
||||
|
||||
@@ -1904,23 +1860,25 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
}));
|
||||
}
|
||||
|
||||
// JAVA 1.8 use Map.Entry.comparingByValue() somehow
|
||||
Map.Entry<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() {
|
||||
@Override
|
||||
public int compare(Map.Entry<Player, Map.Entry<String, Integer>> o1, Map.Entry<Player, Map.Entry<String, Integer>> o2) {
|
||||
return o1.getValue().getValue() - o2.getValue().getValue();
|
||||
if (!data.isEmpty()) {
|
||||
// JAVA 1.8 use Map.Entry.comparingByValue() somehow
|
||||
Map.Entry<Player, Map.Entry<String, Integer>> max = Collections.max(data.entrySet(), new Comparator<Map.Entry<Player, Map.Entry<String, Integer>>>() {
|
||||
@Override
|
||||
public int compare(Map.Entry<Player, Map.Entry<String, Integer>> o1, Map.Entry<Player, Map.Entry<String, Integer>> o2) {
|
||||
return o1.getValue().getValue() - o2.getValue().getValue();
|
||||
}
|
||||
});
|
||||
|
||||
// filter list again by the opponent and a creature of the wanted name that can be targeted
|
||||
list = CardLists.filter(CardLists.filterControlledBy(list, max.getKey()),
|
||||
CardPredicates.nameEquals(max.getValue().getKey()), CardPredicates.isTargetableBy(sa));
|
||||
|
||||
// list should contain only one element or zero
|
||||
sa.resetTargets();
|
||||
for (Card c : list) {
|
||||
sa.getTargets().add(c);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
// filter list again by the opponent and a creature of the wanted name that can be targeted
|
||||
list = CardLists.filter(CardLists.filterControlledBy(list, max.getKey()),
|
||||
CardPredicates.nameEquals(max.getValue().getKey()), CardPredicates.isTargetableBy(sa));
|
||||
|
||||
// list should contain only one element or zero
|
||||
sa.resetTargets();
|
||||
for (Card c : list) {
|
||||
sa.getTargets().add(c);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -2021,17 +1979,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
setPayX = true;
|
||||
// TODO use ComputerUtilCost.getMaxXValue if able
|
||||
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
toPay = ComputerUtilCost.getMaxXValue(sa, ai);
|
||||
} else {
|
||||
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
|
||||
}
|
||||
|
||||
if (toPay == 0) {
|
||||
canBeSaved.add(potentialTgt);
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
if (toPay == 0 || toPay <= usableManaSources) {
|
||||
canBeSaved.add(potentialTgt);
|
||||
}
|
||||
|
||||
|
||||
@@ -245,8 +245,25 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
|
||||
&& !ComputerUtil.isPlayingReanimator(ai);
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Exile)) {
|
||||
// TODO: nothing to do here at the moment
|
||||
return false;
|
||||
String logic = sa.getParam("AILogic");
|
||||
|
||||
if (logic != null && logic.startsWith("DiscardAllAndRetExiled")) {
|
||||
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
|
||||
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
|
||||
|
||||
// minimum card advantage unless the hand will be fully reloaded
|
||||
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
|
||||
|
||||
if (numExiledWithSrc > curHandSize) {
|
||||
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
|
||||
// Try to gain some card advantage if the card will die anyway
|
||||
// TODO: ideally, should evaluate the hand value and not discard good hands to it
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
|
||||
}
|
||||
} else if (origin.equals(ZoneType.Stack)) {
|
||||
// time stop can do something like this:
|
||||
// Origin$ Stack | Destination$ Exile | SubAbility$ DBSkip
|
||||
|
||||
@@ -227,7 +227,7 @@ public class CharmAi extends SpellAbilityAi {
|
||||
if (AiPlayDecision.WillPlay == aic.canPlaySa(sub)) {
|
||||
chosenList.add(sub);
|
||||
if (chosenList.size() == min) {
|
||||
break; // enough choices
|
||||
break; // enough choices
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
@@ -20,6 +21,7 @@ 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.CounterEnumType;
|
||||
import forge.game.combat.Combat;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseType;
|
||||
@@ -73,10 +75,8 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
return !choices.isEmpty();
|
||||
} else if (aiLogic.equals("AtLeast2") || aiLogic.equals("BestBlocker")) {
|
||||
return choices.size() >= 2;
|
||||
} else if (aiLogic.equals("Clone") || aiLogic.equals("Vesuva")) {
|
||||
final String filter = aiLogic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
|
||||
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
|
||||
|
||||
} else if (aiLogic.equals("Clone")) {
|
||||
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
|
||||
choices = CardLists.getValidCards(choices, filter, host.getController(), host, sa);
|
||||
return !choices.isEmpty();
|
||||
} else if (aiLogic.equals("Never")) {
|
||||
@@ -96,12 +96,24 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > ref;
|
||||
}
|
||||
});
|
||||
return !choices.isEmpty();
|
||||
} else if (aiLogic.equals("Ashiok")) {
|
||||
final int loyalty = host.getCounters(CounterEnumType.LOYALTY) - 1;
|
||||
for (int i = loyalty; i >= 0; i--) {
|
||||
sa.setXManaCostPaid(i);
|
||||
choices = ai.getGame().getCardsIn(choiceZone);
|
||||
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), host.getController(), host, sa);
|
||||
if (!choices.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return !choices.isEmpty();
|
||||
} else if (aiLogic.equals("RandomNonLand")) {
|
||||
return !CardLists.getValidCards(choices, "Card.nonLand", host.getController(), host, sa).isEmpty();
|
||||
} else if (aiLogic.equals("Duneblast")) {
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
CardCollection oppCreatures = ai.getWeakestOpponent().getCreaturesInPlay();
|
||||
CardCollection oppCreatures = AiAttackController.choosePreferredDefenderPlayer(ai).getCreaturesInPlay();
|
||||
aiCreatures = CardLists.getNotKeyword(aiCreatures, Keyword.INDESTRUCTIBLE);
|
||||
oppCreatures = CardLists.getNotKeyword(oppCreatures, Keyword.INDESTRUCTIBLE);
|
||||
|
||||
@@ -159,18 +171,13 @@ public class ChooseCardAi extends SpellAbilityAi {
|
||||
options = CardLists.filter(options, Presets.UNTAPPED);
|
||||
}
|
||||
choice = ComputerUtilCard.getBestCreatureAI(options);
|
||||
} else if (logic.equals("Clone") || logic.equals("Vesuva")) {
|
||||
final String filter = logic.equals("Clone") ? "Permanent.YouDontCtrl,Permanent.nonLegendary" :
|
||||
"Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
|
||||
|
||||
} else if (logic.equals("Clone")) {
|
||||
final String filter = "Permanent.YouDontCtrl,Permanent.nonLegendary";
|
||||
CardCollection newOptions = CardLists.getValidCards(options, filter.split(","), ctrl, host, sa);
|
||||
if (!newOptions.isEmpty()) {
|
||||
options = newOptions;
|
||||
}
|
||||
choice = ComputerUtilCard.getBestAI(options);
|
||||
if (logic.equals("Vesuva") && "Vesuva".equals(choice.getName())) {
|
||||
choice = null;
|
||||
}
|
||||
} else if ("RandomNonLand".equals(logic)) {
|
||||
options = CardLists.getValidCards(options, "Card.nonLand", host.getController(), host, sa);
|
||||
choice = Aggregates.random(options);
|
||||
|
||||
@@ -7,6 +7,7 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.StaticData;
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpecialCardAi;
|
||||
@@ -34,9 +35,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
String logic = sa.getParam("AILogic");
|
||||
if (logic.equals("MomirAvatar")) {
|
||||
return SpecialCardAi.MomirVigAvatar.consider(ai, sa);
|
||||
} else if (logic.equals("CursedScroll")) {
|
||||
if (logic.equals("CursedScroll")) {
|
||||
return SpecialCardAi.CursedScroll.consider(ai, sa);
|
||||
}
|
||||
|
||||
@@ -44,7 +43,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
sa.getTargets().add(ai.getWeakestOpponent());
|
||||
sa.getTargets().add(AiAttackController.choosePreferredDefenderPlayer(ai));
|
||||
} else {
|
||||
sa.getTargets().add(ai);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -16,7 +17,7 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
|
||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
Player opp = aiPlayer.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
|
||||
@@ -8,7 +8,6 @@ import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import com.google.common.collect.Sets;
|
||||
|
||||
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
@@ -32,7 +31,6 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbilityCantBeCast;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.collect.FCollection;
|
||||
@@ -54,8 +52,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||
}
|
||||
} else if ("GideonBlackblade".equals(aiLogic)) {
|
||||
return SpecialCardAi.GideonBlackblade.consider(ai, sa);
|
||||
} else if ("SoulEcho".equals(aiLogic)) {
|
||||
return doTriggerAINoCost(ai, sa, true);
|
||||
} else if ("Always".equals(aiLogic)) {
|
||||
return true;
|
||||
}
|
||||
@@ -72,7 +68,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return checkApiLogic(aiPlayer, sa);
|
||||
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -160,6 +156,22 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
return others;
|
||||
} else if ("Counters".equals(logic)) {
|
||||
// TODO: this code will need generalization if this logic is used for cards other
|
||||
// than Elspeth Conquers Death with different choice parameters
|
||||
SpellAbility p1p1 = null, loyalty = null;
|
||||
for (final SpellAbility sp : spells) {
|
||||
if (("P1P1").equals(sp.getParam("CounterType"))) {
|
||||
p1p1 = sp;
|
||||
} else {
|
||||
loyalty = sp;
|
||||
}
|
||||
}
|
||||
if (sa.getParent().getTargetCard() != null && sa.getParent().getTargetCard().getType().isPlaneswalker()) {
|
||||
return loyalty;
|
||||
} else {
|
||||
return p1p1;
|
||||
}
|
||||
} else if ("Fatespinner".equals(logic)) {
|
||||
SpellAbility skipDraw = null, /*skipMain = null,*/ skipCombat = null;
|
||||
for (final SpellAbility sp : spells) {
|
||||
@@ -230,10 +242,11 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||
return allow;
|
||||
}
|
||||
|
||||
SpellAbility firstSpell = imprinted.getFirstSpellAbility();
|
||||
// check if something would prevent it from casting
|
||||
if (firstSpell == null || StaticAbilityCantBeCast.cantBeCastAbility(firstSpell, imprinted, owner)) {
|
||||
return allow;
|
||||
//if Iona does prevent from casting, allow it to draw
|
||||
for (final Card io : player.getCardsIn(ZoneType.Battlefield, "Iona, Shield of Emeria")) {
|
||||
if (CardUtil.getColors(imprinted).hasAnyColor(MagicColor.fromName(io.getChosenColor()))) {
|
||||
return allow;
|
||||
}
|
||||
}
|
||||
|
||||
if (dmg == 0) {
|
||||
@@ -364,8 +377,6 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
|
||||
} else if ("Riot".equals(logic)) {
|
||||
SpellAbility counterSA = spells.get(0), hasteSA = spells.get(1);
|
||||
return preferHasteForRiot(sa, player) ? hasteSA : counterSA;
|
||||
} else if ("CrawlingBarrens".equals(logic)) {
|
||||
return SpecialCardAi.CrawlingBarrens.considerAnimating(player, sa, spells);
|
||||
}
|
||||
return spells.get(0); // return first choice if no logic found
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -16,7 +17,7 @@ public class ChooseNumberAi extends SpellAbilityAi {
|
||||
TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
Player opp = aiPlayer.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -54,7 +55,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
} else {
|
||||
|
||||
@@ -3,13 +3,17 @@ package forge.ai.ability;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -52,7 +56,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
bFlag |= (!c.isCreature() && !c.isTapped() && !(c.getTurnInZone() == phase.getTurn()));
|
||||
|
||||
// for creatures that could be improved (like Figure of Destiny)
|
||||
if (c.isCreature() && (sa.hasParam("Permanent") || (!c.isTapped() && !c.isSick()))) {
|
||||
if (c.isCreature() && (!sa.hasParam("Duration") || (!c.isTapped() && !c.isSick()))) {
|
||||
int power = -5;
|
||||
if (sa.hasParam("Power")) {
|
||||
power = AbilityUtils.calculateAmount(source, sa.getParam("Power"), sa);
|
||||
@@ -68,8 +72,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
}
|
||||
|
||||
if (!bFlag) { // All of the defined stuff is cloned, not very
|
||||
// useful
|
||||
if (!bFlag) { // All of the defined stuff is cloned, not very useful
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
@@ -95,11 +98,18 @@ public class CloneAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
Card host = sa.getHostCard();
|
||||
boolean chance = true;
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
chance = cloneTgtAI(sa);
|
||||
} else {
|
||||
if (sa.hasParam("Choices")) {
|
||||
CardCollectionView choices = CardLists.getValidCards(host.getGame().getCardsIn(ZoneType.Battlefield),
|
||||
sa.getParam("Choices"), host.getController(), host, sa);
|
||||
|
||||
chance = !choices.isEmpty();
|
||||
}
|
||||
}
|
||||
|
||||
// Improve AI for triggers. If source is a creature with:
|
||||
@@ -171,18 +181,18 @@ public class CloneAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
|
||||
Player targetedPlayer, Map<String, Object> params) {
|
||||
|
||||
final Card host = sa.getHostCard();
|
||||
final String name = host.getName();
|
||||
final Player ctrl = host.getController();
|
||||
|
||||
final Card cloneTarget = getCloneTarget(sa);
|
||||
final boolean isOpp = cloneTarget.getController().isOpponentOf(sa.getActivatingPlayer());
|
||||
|
||||
final boolean isVesuva = "Vesuva".equals(host.getName());
|
||||
final boolean isVesuva = "Vesuva".equals(name) || "Sculpting Steel".equals(name);
|
||||
final boolean canCloneLegendary = "True".equalsIgnoreCase(sa.getParam("NonLegendary"));
|
||||
|
||||
String filter = !isVesuva ? "Permanent.YouDontCtrl,Permanent.nonLegendary"
|
||||
: "Permanent.YouDontCtrl+notnamedVesuva,Permanent.nonLegendary+notnamedVesuva";
|
||||
: "Permanent.YouDontCtrl+notnamed" + name + ",Permanent.nonLegendary+notnamed" + name;
|
||||
|
||||
// TODO: rewrite this block so that this is done somehow more elegantly
|
||||
if (canCloneLegendary) {
|
||||
@@ -201,12 +211,13 @@ public class CloneAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);
|
||||
|
||||
if (isVesuva && "Vesuva".equals(choice.getName())) {
|
||||
choice = null;
|
||||
// prevent loop of choosing copy of same card
|
||||
if (isVesuva) {
|
||||
options = CardLists.filter(options, Predicates.not(CardPredicates.sharesNameWith(host)));
|
||||
}
|
||||
|
||||
Card choice = isOpp ? ComputerUtilCard.getWorstAI(options) : ComputerUtilCard.getBestAI(options);
|
||||
|
||||
return choice;
|
||||
}
|
||||
|
||||
@@ -234,7 +245,7 @@ public class CloneAi extends SpellAbilityAi {
|
||||
// Combat_Begin step
|
||||
if (!ph.is(PhaseType.COMBAT_BEGIN)
|
||||
&& ph.isPlayerTurn(ai) && !SpellAbilityAi.isSorcerySpeed(sa)
|
||||
&& !sa.hasParam("ActivationPhases") && !sa.hasParam("Permanent")) {
|
||||
&& !sa.hasParam("ActivationPhases") && sa.hasParam("Duration")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -245,6 +256,6 @@ public class CloneAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// don't activate during main2 unless this effect is permanent
|
||||
return !ph.is(PhaseType.MAIN2) || sa.hasParam("Permanent");
|
||||
return !ph.is(PhaseType.MAIN2) || !sa.hasParam("Duration");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
@@ -29,9 +30,8 @@ public class ControlExchangeAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
|
||||
CardCollection list =
|
||||
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on
|
||||
// purpose
|
||||
CardLists.getValidCards(AiAttackController.choosePreferredDefenderPlayer(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on purpose
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
|
||||
@@ -39,6 +39,7 @@ import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
|
||||
@@ -109,8 +110,7 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// Don't steal something if I can't Attack without, or prevent it from
|
||||
// blocking at least
|
||||
// Don't steal something if I can't Attack without, or prevent it from blocking at least
|
||||
if (lose.contains("EOT")
|
||||
&& ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
&& !sa.isTrigger()) {
|
||||
@@ -210,7 +210,12 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO check life of controller and consider stealing from another opponent so the risk of your army disappearing is spread out
|
||||
while (t == null) {
|
||||
// filter by MustTarget requirement
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
if (planeswalkers > 0) {
|
||||
t = ComputerUtilCard.getBestPlaneswalkerAI(list);
|
||||
} else if (creatures > 0) {
|
||||
@@ -238,6 +243,11 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
enchantments--;
|
||||
}
|
||||
|
||||
// Restore original list for next loop if filtered by MustTarget requirement
|
||||
if (mustTargetFiltered) {
|
||||
list = originalList;
|
||||
}
|
||||
|
||||
if (!sa.canTarget(t)) {
|
||||
list.remove(t);
|
||||
t = null;
|
||||
@@ -254,7 +264,6 @@ public class ControlGainAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -44,7 +44,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ("MimicVat".equals(aiLogic)) {
|
||||
if ("MomirAvatar".equals(aiLogic)) {
|
||||
return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
|
||||
} else if ("MimicVat".equals(aiLogic)) {
|
||||
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
|
||||
} else if ("AtEOT".equals(aiLogic)) {
|
||||
return ph.is(PhaseType.END_OF_TURN);
|
||||
|
||||
@@ -113,8 +113,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
boolean setPayX = false;
|
||||
if (unlessCost.equals("X") && sa.getSVar(unlessCost).equals("Count$xPaid")) {
|
||||
setPayX = true;
|
||||
// TODO use ComputerUtilCost.getMaxXValue
|
||||
toPay = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), usableManaSources + 1);
|
||||
toPay = Math.min(ComputerUtilCost.getMaxXValue(sa, ai), usableManaSources + 1);
|
||||
} else {
|
||||
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
|
||||
}
|
||||
@@ -124,8 +123,7 @@ public class CounterAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (toPay <= usableManaSources) {
|
||||
// If this is a reusable Resource, feel free to play it most of
|
||||
// the time
|
||||
// If this is a reusable Resource, feel free to play it most of the time
|
||||
if (!SpellAbilityAi.playReusable(ai,sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -372,54 +372,57 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
List<Card> aiList = CardLists.filterControlledBy(tgtCards, ai);
|
||||
if (!aiList.isEmpty()) {
|
||||
List<Card> best = CardLists.filter(aiList, new Predicate<Card>() {
|
||||
Card lki = CardUtil.getLKICopy(src);
|
||||
lki.clearCounters();
|
||||
// go for opponent when value implies debuff
|
||||
if (ComputerUtilCard.evaluateCreature(src) > ComputerUtilCard.evaluateCreature(lki)) {
|
||||
List<Card> aiList = CardLists.filterControlledBy(tgtCards, ai);
|
||||
if (!aiList.isEmpty()) {
|
||||
List<Card> best = CardLists.filter(aiList, new Predicate<Card>() {
|
||||
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// gain from useless
|
||||
if (ComputerUtilCard.isUselessCreature(ai, card)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// source would leave the game
|
||||
if (card.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cType != null) {
|
||||
if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) {
|
||||
return false;
|
||||
}
|
||||
if (cType.is(CounterEnumType.M1M1) && card.hasKeyword(Keyword.PERSIST)) {
|
||||
@Override
|
||||
public boolean apply(Card card) {
|
||||
// gain from useless
|
||||
if (ComputerUtilCard.isUselessCreature(ai, card)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!card.canReceiveCounters(cType)) {
|
||||
// source would leave the game
|
||||
if (card.hasSVar("EndOfTurnLeavePlay")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (cType != null) {
|
||||
if (cType.is(CounterEnumType.P1P1) && card.hasKeyword(Keyword.UNDYING)) {
|
||||
return false;
|
||||
}
|
||||
if (cType.is(CounterEnumType.M1M1)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!card.canReceiveCounters(cType)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (best.isEmpty()) {
|
||||
best = aiList;
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getBestCreatureAI(best);
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
if (best.isEmpty()) {
|
||||
best = aiList;
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getBestCreatureAI(best);
|
||||
|
||||
if (card != null) {
|
||||
sa.getTargets().add(card);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// move counter to opponents creature but only if you can not steal
|
||||
// them
|
||||
// try to move to something useless or something that would leave
|
||||
// play
|
||||
// move counter to opponents creature but only if you can not steal them
|
||||
// try to move to something useless or something that would leave play
|
||||
List<Card> oppList = CardLists.filterControlledBy(tgtCards, ai.getOpponents());
|
||||
if (!oppList.isEmpty()) {
|
||||
List<Card> best = CardLists.filter(oppList, new Predicate<Card>() {
|
||||
@@ -441,7 +444,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
});
|
||||
|
||||
if (best.isEmpty()) {
|
||||
best = aiList;
|
||||
best = oppList;
|
||||
}
|
||||
|
||||
Card card = ComputerUtilCard.getBestCreatureAI(best);
|
||||
@@ -455,7 +458,7 @@ public class CountersMoveAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// used for multiple sources -> defied
|
||||
// used for multiple sources -> defined
|
||||
// or for source -> multiple defined
|
||||
@Override
|
||||
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional,
|
||||
|
||||
@@ -120,8 +120,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
|
||||
|
||||
// pre filter targetable cards with counters and can receive one of
|
||||
// them
|
||||
// pre filter targetable cards with counters and can receive one of them
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,8 +8,10 @@ import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiProps;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.GameEntity;
|
||||
import forge.game.card.Card;
|
||||
@@ -121,6 +123,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
||||
|
||||
final CounterType poison = CounterType.get(CounterEnumType.POISON);
|
||||
|
||||
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
|
||||
// because countertype can't be chosen anymore, only look for posion counters
|
||||
for (final Player p : Iterables.filter(options, Player.class)) {
|
||||
if (p.isOpponentOf(ai)) {
|
||||
@@ -128,7 +131,8 @@ public class CountersProliferateAi extends SpellAbilityAi {
|
||||
return (T)p;
|
||||
}
|
||||
} else {
|
||||
if (p.getCounters(poison) <= 5 || p.canReceiveCounters(poison)) {
|
||||
// poison is risky, should not proliferate them in most cases
|
||||
if ((p.getCounters(poison) <= 5 && aggroAI && p.getCounters(CounterEnumType.EXPERIENCE) + p.getCounters(CounterEnumType.ENERGY) >= 1) || !p.canReceiveCounters(poison)) {
|
||||
return (T)p;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ import forge.game.player.PlayerCollection;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
@@ -313,7 +315,12 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
} else if (logic.startsWith("MoveCounter")) {
|
||||
return doMoveCounterLogic(ai, sa, ph);
|
||||
} else if (logic.equals("CrawlingBarrens")) {
|
||||
return SpecialCardAi.CrawlingBarrens.consider(ai, sa);
|
||||
boolean willActivate = SpecialCardAi.CrawlingBarrens.consider(ai, sa);
|
||||
if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
// don't use this for mana until after combat
|
||||
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
|
||||
}
|
||||
return willActivate;
|
||||
}
|
||||
|
||||
if (!sa.metConditions() && sa.getSubAbility() == null) {
|
||||
@@ -401,19 +408,37 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if ("Polukranos".equals(logic)) {
|
||||
boolean found = false;
|
||||
for (Trigger tr : source.getTriggers()) {
|
||||
if (!tr.getMode().equals(TriggerType.BecomeMonstrous)) {
|
||||
continue;
|
||||
}
|
||||
SpellAbility oa = tr.ensureAbility();
|
||||
if (oa == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
CardCollection targets = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), sa);
|
||||
// need to set Activating player
|
||||
oa.setActivatingPlayer(ai);
|
||||
CardCollection targets = CardLists.getTargetableCards(ai.getOpponents().getCreaturesInPlay(), oa);
|
||||
|
||||
if (!targets.isEmpty()){
|
||||
boolean canSurvive = false;
|
||||
for (Card humanCreature : targets) {
|
||||
if (!FightAi.canKill(humanCreature, source, 0)){
|
||||
canSurvive = true;
|
||||
if (!targets.isEmpty()){
|
||||
boolean canSurvive = false;
|
||||
for (Card humanCreature : targets) {
|
||||
if (!FightAi.canKill(humanCreature, source, 0)){
|
||||
canSurvive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!canSurvive){
|
||||
return false;
|
||||
}
|
||||
if (!canSurvive){
|
||||
return false;
|
||||
}
|
||||
};
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
if (!found) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -841,17 +866,16 @@ public class CountersPutAi extends SpellAbilityAi {
|
||||
choice = Aggregates.random(list);
|
||||
}
|
||||
}
|
||||
if (choice != null && divided) {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
|
||||
sa.addDividedAllocation(choice, left);
|
||||
} else {
|
||||
sa.addDividedAllocation(choice, alloc);
|
||||
left -= alloc;
|
||||
}
|
||||
}
|
||||
if (choice != null && divided) {
|
||||
int alloc = Math.max(amount / totalTargets, 1);
|
||||
if (sa.getTargets().size() == Math.min(totalTargets, sa.getMaxTargets()) - 1) {
|
||||
sa.addDividedAllocation(choice, left);
|
||||
} else {
|
||||
sa.addDividedAllocation(choice, alloc);
|
||||
left -= alloc;
|
||||
}
|
||||
}
|
||||
|
||||
if (choice != null) {
|
||||
sa.getTargets().add(choice);
|
||||
list.remove(choice);
|
||||
|
||||
@@ -14,7 +14,7 @@ import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerDamageDone;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
|
||||
@@ -56,7 +56,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
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")) {
|
||||
sa.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
|
||||
@@ -75,7 +75,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
// 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 (trig.getMode() == TriggerType.DamageDone) {
|
||||
if (("Opponent".equals(trig.getParam("ValidTarget")))
|
||||
&& (!"True".equals(trig.getParam("CombatDamage")))) {
|
||||
return true;
|
||||
@@ -111,8 +111,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
final CardCollectionView hand = comp.getCardsIn(ZoneType.Hand);
|
||||
|
||||
if ((enemy.getLife() - restDamage) < 5) {
|
||||
// drop the human to less than 5
|
||||
// life
|
||||
// drop the human to less than 5 life
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -147,12 +146,12 @@ public abstract class DamageAiBase extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
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++) {
|
||||
value *= 1.1f;
|
||||
}
|
||||
}
|
||||
if (value < 0.2f) { //hard floor to reduce ridiculous odds for instants over time
|
||||
if (value < 0.2f) { //hard floor to reduce ridiculous odds for instants over time
|
||||
return false;
|
||||
} else {
|
||||
final float chance = MyRandom.getRandom().nextFloat();
|
||||
|
||||
@@ -30,22 +30,20 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardFactoryUtil;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.cost.CostPartMana;
|
||||
import forge.game.cost.CostRemoveCounter;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetChoices;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.Aggregates;
|
||||
import forge.util.MyRandom;
|
||||
@@ -131,7 +129,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
// Set PayX here to maximum value. It will be adjusted later depending on the target.
|
||||
sa.setXManaCostPaid(dmg);
|
||||
} else if (sa.getSVar(damage).contains("InYourHand") && source.isInZone(ZoneType.Hand)) {
|
||||
dmg = CardFactoryUtil.xCount(source, sa.getSVar(damage)) - 1; // the card will be spent casting the spell, so actual damage is 1 less
|
||||
dmg = AbilityUtils.calculateAmount(source, damage, sa) - 1; // 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()) {
|
||||
@@ -343,6 +341,9 @@ public class DamageDealAi extends DamageAiBase {
|
||||
final Game game = source.getGame();
|
||||
List<Card> hPlay = getTargetableCards(ai, sa, pl, tgt, activator, source, game);
|
||||
|
||||
// Filter MustTarget requirements
|
||||
StaticAbilityMustTarget.filterMustTargetCards(ai, hPlay, sa);
|
||||
|
||||
CardCollection killables = CardLists.filter(hPlay, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
@@ -437,63 +438,12 @@ public class DamageDealAi extends DamageAiBase {
|
||||
|
||||
// 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 ComputerUtilCard.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(CounterEnumType.LOYALTY);
|
||||
int pwScore = curLoyalty * 10;
|
||||
|
||||
for (SpellAbility sa : pw.getSpellAbilities()) {
|
||||
if (sa.hasParam("Ultimate")) {
|
||||
Integer 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 != null && loyaltyCost != 0 && loyaltyCost - curLoyalty <= 1) {
|
||||
// Will ultimate soon
|
||||
pwScore += 10000;
|
||||
}
|
||||
|
||||
if (pwScore > bestScore) {
|
||||
bestScore = pwScore;
|
||||
bestTgt = pw;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return bestTgt;
|
||||
}
|
||||
|
||||
private Card getWorstPlaneswalkerToDamage(final List<Card> pws) {
|
||||
Card bestTgt = null;
|
||||
|
||||
int bestScore = Integer.MAX_VALUE;
|
||||
for (Card pw : pws) {
|
||||
int curLoyalty = pw.getCounters(CounterEnumType.LOYALTY);
|
||||
|
||||
if (curLoyalty < bestScore) {
|
||||
bestScore = curLoyalty;
|
||||
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);
|
||||
|
||||
@@ -730,12 +680,10 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
|
||||
// When giving priority to targeting Creatures for mandatory
|
||||
// triggers
|
||||
// feel free to add the Human after we run out of good targets
|
||||
// triggers feel free to add the Human after we run out of good targets
|
||||
|
||||
// TODO: add check here if card is about to die from something
|
||||
// on the stack
|
||||
// or from taking combat damage
|
||||
// on the stack or from taking combat damage
|
||||
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
boolean freePing = immediately || abCost == null
|
||||
@@ -792,8 +740,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: Improve Damage, we shouldn't just target the player just
|
||||
// because we can
|
||||
// TODO: Improve Damage, we shouldn't just target the player just because we can
|
||||
if (sa.canTarget(enemy) && tcs.size() < tgt.getMaxTargets(source, sa)) {
|
||||
if (((phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai))
|
||||
|| (SpellAbilityAi.isSorcerySpeed(sa) && phase.is(PhaseType.MAIN2))
|
||||
@@ -951,7 +898,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
}
|
||||
else if (tgt.canTgtPlaneswalker()) {
|
||||
// Second pass for planeswalkers: choose AI's worst planeswalker
|
||||
final Card c = getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.PLANESWALKERS), CardPredicates.isTargetableBy(sa)));
|
||||
final Card c = ComputerUtilCard.getWorstPlaneswalkerToDamage(CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.PLANESWALKERS), CardPredicates.isTargetableBy(sa)));
|
||||
if (c != null) {
|
||||
sa.getTargets().add(c);
|
||||
if (divided) {
|
||||
@@ -981,7 +928,6 @@ public class DamageDealAi extends DamageAiBase {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
final String damage = sa.getParam("NumDmg");
|
||||
int dmg = AbilityUtils.calculateAmount(source, damage, sa);
|
||||
@@ -1038,7 +984,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
saTgt = saTgt.getParent();
|
||||
}
|
||||
|
||||
Player opponent = ai.getOpponents().min(PlayerPredicates.compareByLife());
|
||||
Player opponent = ai.getWeakestOpponent();
|
||||
|
||||
// TODO: somehow account for the possible cost reduction?
|
||||
int dmg = ComputerUtilMana.determineLeftoverMana(sa, ai, saTgt.getParam("XColor"));
|
||||
@@ -1081,7 +1027,7 @@ public class DamageDealAi extends DamageAiBase {
|
||||
saTgt.resetTargets();
|
||||
saTgt.getTargets().add(tgtCreature != null && dmg < opponent.getLife() ? tgtCreature : opponent);
|
||||
|
||||
sa.setXManaCostPaid(dmg);
|
||||
saTgt.setXManaCostPaid(dmg);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
@@ -26,9 +27,6 @@ import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class DebuffAi extends SpellAbilityAi {
|
||||
// *************************************************************************
|
||||
// ***************************** Debuff ************************************
|
||||
// *************************************************************************
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
|
||||
@@ -140,7 +138,6 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
|
||||
while (sa.getTargets().size() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
|
||||
Card t = null;
|
||||
// boolean goodt = false;
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
|
||||
@@ -176,19 +173,18 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
* @return a CardCollection.
|
||||
*/
|
||||
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
|
||||
if (!list.isEmpty()) {
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.hasAnyKeyword(kws); // don't add duplicate negative
|
||||
// keywords
|
||||
return c.hasAnyKeyword(kws); // don't add duplicate negative keywords
|
||||
}
|
||||
});
|
||||
}
|
||||
return list;
|
||||
} // getCurseCreatures()
|
||||
}
|
||||
|
||||
/**
|
||||
* <p>
|
||||
@@ -216,7 +212,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
list.remove(c);
|
||||
}
|
||||
|
||||
final CardCollection pref = CardLists.filterControlledBy(list, ai.getWeakestOpponent());
|
||||
final CardCollection pref = CardLists.filterControlledBy(list, ai.getOpponents());
|
||||
final CardCollection forced = CardLists.filterControlledBy(list, ai);
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
@@ -242,8 +238,7 @@ public class DebuffAi extends SpellAbilityAi {
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO - if forced targeting, just pick something without the given
|
||||
// keyword
|
||||
// TODO - if forced targeting, just pick something without the given keyword
|
||||
Card c;
|
||||
if (CardLists.getNotType(forced, "Creature").size() == 0) {
|
||||
c = ComputerUtilCard.getWorstCreatureAI(forced);
|
||||
|
||||
@@ -2,15 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import com.google.common.base.Predicate;
|
||||
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.AiProps;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpecialAiLogic;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.ai.*;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
@@ -26,6 +18,7 @@ import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class DestroyAi extends SpellAbilityAi {
|
||||
@@ -122,17 +115,21 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
|
||||
CardCollection list;
|
||||
|
||||
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
// Targeting
|
||||
if (sa.usesTargeting()) {
|
||||
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
|
||||
// (e.g. Heliod's Intervention)
|
||||
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
int xPay = ComputerUtilCost.getMaxXValue(sa, ai);
|
||||
sa.getRootAbility().setXManaCostPaid(xPay);
|
||||
}
|
||||
|
||||
// Assume there where already enough targets chosen by AI Logic Above
|
||||
if (!sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
|
||||
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -140,11 +137,11 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
int maxTargets;
|
||||
|
||||
if (sa.costHasManaX()) {
|
||||
if (sa.getRootAbility().costHasManaX()) {
|
||||
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
|
||||
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai);
|
||||
// need to set XPaid to get the right number for
|
||||
sa.setXManaCostPaid(maxTargets);
|
||||
sa.getRootAbility().setXManaCostPaid(maxTargets);
|
||||
// need to check for maxTargets
|
||||
maxTargets = Math.min(maxTargets, sa.getMaxTargets());
|
||||
} else {
|
||||
@@ -226,6 +223,10 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
// target loop
|
||||
// TODO use can add more Targets
|
||||
while (sa.getTargets().size() < maxTargets) {
|
||||
// filter by MustTarget requirement
|
||||
CardCollection originalList = new CardCollection(list);
|
||||
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
|
||||
|
||||
if (list.isEmpty()) {
|
||||
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
|
||||
sa.resetTargets();
|
||||
@@ -286,6 +287,12 @@ public class DestroyAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Restore original list for next loop if filtered by MustTarget requirement
|
||||
if (mustTargetFiltered) {
|
||||
list = originalList;
|
||||
}
|
||||
|
||||
list.remove(choice);
|
||||
sa.getTargets().add(choice);
|
||||
}
|
||||
|
||||
@@ -70,12 +70,12 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
return doMassRemovalLogic(ai, sa);
|
||||
}
|
||||
|
||||
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
|
||||
final Card source = sa.getHostCard();
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
Player opponent = ai.getWeakestOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()?
|
||||
|
||||
final int CREATURE_EVAL_THRESHOLD = 200;
|
||||
// if we hit the whole board, the other opponents who are not the reason to cast this probably still suffer a bit too
|
||||
final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1);
|
||||
|
||||
if (logic.equals("Always")) {
|
||||
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
|
||||
@@ -93,99 +93,101 @@ public class DestroyAllAi extends SpellAbilityAi {
|
||||
valid = valid.replace("X", Integer.toString(xPay));
|
||||
}
|
||||
|
||||
CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield),
|
||||
valid.split(","), source.getController(), source, sa);
|
||||
CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
|
||||
source.getController(), source, sa);
|
||||
|
||||
opplist = CardLists.filter(opplist, predicate);
|
||||
ailist = CardLists.filter(ailist, predicate);
|
||||
if (opplist.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// TODO should probably sort results when targeted to use on biggest threat instead of first match
|
||||
for (Player opponent: ai.getOpponents()) {
|
||||
CardCollection opplist = CardLists.getValidCards(opponent.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
CardCollection ailist = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opponent)) {
|
||||
sa.getTargets().add(opponent);
|
||||
ailist.clear();
|
||||
} else {
|
||||
opplist = CardLists.filter(opplist, predicate);
|
||||
ailist = CardLists.filter(ailist, predicate);
|
||||
if (opplist.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Special handling for Raiding Party
|
||||
if (logic.equals("RaidingParty")) {
|
||||
int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size());
|
||||
int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size());
|
||||
|
||||
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
|
||||
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if only creatures are affected evaluate both lists and pass only if
|
||||
// human creatures are more valuable
|
||||
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 = containsAttacker | opplist.contains(att);
|
||||
}
|
||||
}
|
||||
if (!containsAttacker) {
|
||||
return false;
|
||||
}
|
||||
AiBlockController block = new AiBlockController(ai);
|
||||
block.assignBlockersForCombat(combat);
|
||||
|
||||
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} // only lands involved
|
||||
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
|
||||
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds") && !opplist.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
CardCollection oppCreatures = opponent.getCreaturesInPlay();
|
||||
if (!oppCreatures.isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opponent)) {
|
||||
sa.getTargets().add(opponent);
|
||||
ailist.clear();
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// check if the AI would lose more lands than the opponent would
|
||||
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) {
|
||||
|
||||
// Special handling for Raiding Party
|
||||
if (logic.equals("RaidingParty")) {
|
||||
int numAiCanSave = Math.min(CardLists.filter(ai.getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, ailist.size());
|
||||
int numOppsCanSave = Math.min(CardLists.filter(ai.getOpponents().getCreaturesInPlay(), Predicates.and(CardPredicates.isColor(MagicColor.WHITE), CardPredicates.Presets.UNTAPPED)).size() * 2, opplist.size());
|
||||
|
||||
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
|
||||
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
|
||||
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
|
||||
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// if only creatures are affected evaluate both lists and pass only if human creatures are more valuable
|
||||
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 = containsAttacker | opplist.contains(att);
|
||||
}
|
||||
}
|
||||
if (!containsAttacker) {
|
||||
return false;
|
||||
}
|
||||
AiBlockController block = new AiBlockController(ai);
|
||||
block.assignBlockersForCombat(combat);
|
||||
|
||||
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
} // only lands involved
|
||||
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
|
||||
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) {
|
||||
return true;
|
||||
}
|
||||
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
|
||||
CardCollection aiCreatures = ai.getCreaturesInPlay();
|
||||
CardCollection oppCreatures = opponent.getCreaturesInPlay();
|
||||
if (!oppCreatures.isEmpty()) {
|
||||
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// check if the AI would lose more lands than the opponent would
|
||||
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) {
|
||||
return false;
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
|
||||
return false;
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import java.util.Map;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
@@ -32,7 +33,7 @@ public class DigAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
Player libraryOwner = ai;
|
||||
|
||||
@@ -44,9 +45,8 @@ public class DigAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
return false;
|
||||
} else {
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ public class DigAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final SpellAbility root = sa.getRootAbility();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (mandatory && sa.canTarget(opp)) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
@@ -19,7 +20,7 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final Card host = sa.getHostCard();
|
||||
Player libraryOwner = ai;
|
||||
|
||||
@@ -27,9 +28,8 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
return false;
|
||||
} else {
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public class DigMultipleAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (mandatory && sa.canTarget(opp)) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
@@ -31,10 +32,8 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
chance = 1;
|
||||
}
|
||||
|
||||
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||
|
||||
Player libraryOwner = ai;
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
|
||||
if ("DontMillSelf".equals(logic)) {
|
||||
// A card that digs for specific things and puts everything revealed before it into graveyard
|
||||
@@ -61,9 +60,8 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
if (!sa.canTarget(opp)) {
|
||||
return false;
|
||||
} else {
|
||||
sa.getTargets().add(opp);
|
||||
}
|
||||
sa.getTargets().add(opp);
|
||||
libraryOwner = opp;
|
||||
} else {
|
||||
if (sa.hasParam("Valid")) {
|
||||
@@ -92,12 +90,12 @@ public class DigUntilAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
sa.resetTargets();
|
||||
if (sa.isCurse()) {
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
@@ -60,11 +61,9 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
if (players.get(0) == ai) {
|
||||
// the ai should only be using something like this if he has
|
||||
// few cards in hand,
|
||||
// cards like this better have a good drawback to be in the
|
||||
// AIs deck
|
||||
// cards like this better have a good drawback to be in the AIs deck
|
||||
} else {
|
||||
// defined to the human, so that's fine as long the human
|
||||
// has cards
|
||||
// defined to the human, so that's fine as long the human has cards
|
||||
if (!humanHasHand) {
|
||||
return false;
|
||||
}
|
||||
@@ -170,7 +169,7 @@ public class DiscardAi extends SpellAbilityAi {
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (!discardTargetAI(ai, sa)) {
|
||||
if (mandatory && sa.canTarget(opp)) {
|
||||
sa.getTargets().add(opp);
|
||||
|
||||
@@ -23,8 +23,7 @@ public class DrainManaAi extends SpellAbilityAi {
|
||||
|
||||
if (tgt == null) {
|
||||
// assume we are looking to tap human's stuff
|
||||
// TODO - check for things with untap abilities, and don't tap
|
||||
// those.
|
||||
// TODO - check for things with untap abilities, and don't tap those.
|
||||
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
|
||||
|
||||
if (!defined.contains(opp)) {
|
||||
|
||||
@@ -257,7 +257,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
} else {
|
||||
numCards = ComputerUtilCost.getMaxXValue(sa, ai);
|
||||
// try not to overdraw
|
||||
int safeDraw = Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3);
|
||||
int safeDraw = Math.abs(Math.min(computerMaxHandSize - computerHandSize, computerLibrarySize - 3));
|
||||
if (sa.getHostCard().isInstant() || sa.getHostCard().isSorcery()) { safeDraw++; } // card will be spent
|
||||
numCards = Math.min(numCards, safeDraw);
|
||||
|
||||
@@ -377,7 +377,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
// checks what the ai prevent from casting it on itself
|
||||
// if spell is not mandatory
|
||||
if (aiTarget && !ai.cantLose()) {
|
||||
if (numCards >= computerLibrarySize) {
|
||||
if (numCards >= computerLibrarySize - 3) {
|
||||
if (xPaid) {
|
||||
numCards = computerLibrarySize - 1;
|
||||
if (numCards <= 0 && !mandatory) {
|
||||
@@ -422,8 +422,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
}
|
||||
root.setXManaCostPaid(numCards);
|
||||
} else {
|
||||
// Don't draw too many cards and then risk discarding
|
||||
// cards at EOT
|
||||
// Don't draw too many cards and then risk discarding cards at EOT
|
||||
if (!drawback && !mandatory) {
|
||||
return false;
|
||||
}
|
||||
@@ -441,7 +440,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
continue;
|
||||
}
|
||||
|
||||
// use xPaid abilties only for itself
|
||||
// use xPaid abilities only for itself
|
||||
if (xPaid) {
|
||||
continue;
|
||||
}
|
||||
@@ -493,7 +492,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
// TODO: consider if human is the defined player
|
||||
|
||||
// ability is not targeted
|
||||
if (numCards >= computerLibrarySize) {
|
||||
if (numCards >= computerLibrarySize - 3) {
|
||||
if (ai.isCardInPlay("Laboratory Maniac")) {
|
||||
return true;
|
||||
}
|
||||
@@ -509,8 +508,7 @@ public class DrawAi extends SpellAbilityAi {
|
||||
&& game.getPhaseHandler().isPlayerTurn(ai)
|
||||
&& !sa.isTrigger()
|
||||
&& !assumeSafeX) {
|
||||
// Don't draw too many cards and then risk discarding cards at
|
||||
// EOT
|
||||
// Don't draw too many cards and then risk discarding cards at EOT
|
||||
if (!drawback) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -79,7 +79,7 @@ public class EffectAi extends SpellAbilityAi {
|
||||
if (!game.getStack().isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
|
||||
if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
|
||||
return false;
|
||||
}
|
||||
if (!ComputerUtilCombat.lifeInDanger(ai, game.getCombat())) {
|
||||
|
||||
@@ -19,6 +19,7 @@ import forge.game.keyword.Keyword;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.staticability.StaticAbilityMustTarget;
|
||||
import forge.game.trigger.Trigger;
|
||||
import forge.game.trigger.TriggerType;
|
||||
import forge.util.MyRandom;
|
||||
@@ -50,6 +51,9 @@ public class FightAi extends SpellAbilityAi {
|
||||
aiCreatures = ComputerUtil.getSafeTargets(ai, sa, aiCreatures);
|
||||
List<Card> humCreatures = ai.getOpponents().getCreaturesInPlay();
|
||||
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
|
||||
// Filter MustTarget requirements
|
||||
StaticAbilityMustTarget.filterMustTargetCards(ai, humCreatures, sa);
|
||||
|
||||
if (humCreatures.isEmpty())
|
||||
return false; //prevent IndexOutOfBoundsException on MOJHOSTO variant
|
||||
|
||||
|
||||
@@ -16,14 +16,32 @@ public class FlipACoinAi extends SpellAbilityAi {
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String AILogic = sa.getParam("AILogic");
|
||||
if (AILogic.equals("Never")) {
|
||||
String ailogic = sa.getParam("AILogic");
|
||||
if (ailogic.equals("Never")) {
|
||||
return false;
|
||||
} else if (AILogic.equals("PhaseOut")) {
|
||||
} else if (ailogic.equals("PhaseOut")) {
|
||||
if (!ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(sa.getHostCard())) {
|
||||
return false;
|
||||
}
|
||||
} else if (AILogic.equals("KillOrcs")) {
|
||||
} else if (ailogic.equals("Bangchuckers")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
|
||||
return false;
|
||||
}
|
||||
sa.resetTargets();
|
||||
for (Player o : ai.getOpponents()) {
|
||||
if (sa.canTarget(o) && o.canLoseLife() && !o.cantLose()) {
|
||||
sa.getTargets().add(o);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
for (Card c : ai.getOpponents().getCreaturesInPlay()) {
|
||||
if (sa.canTarget(c)) {
|
||||
sa.getTargets().add(c);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
} else if (ailogic.equals("KillOrcs")) {
|
||||
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.END_OF_TURN) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -29,7 +29,7 @@ public class FogAi extends SpellAbilityAi {
|
||||
final Card hostCard = sa.getHostCard();
|
||||
|
||||
// Don't cast it, if the effect is already in place
|
||||
if (game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
|
||||
if (game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,7 @@ public class GameLossAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only one SA Lose the Game card right now, which is Door to
|
||||
// Nothingness
|
||||
// Only one SA Lose the Game card right now, which is Door to Nothingness
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
@@ -29,20 +28,23 @@ public class GameLossAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
Player loser = ai;
|
||||
|
||||
// Phage the Untouchable
|
||||
// (Final Fortune would need to attach it's delayed trigger to a
|
||||
// specific turn, which can't be done yet)
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
if (ai.getGame().getCombat() != null) {
|
||||
loser = ai.getGame().getCombat().getDefenderPlayerByAttacker(sa.getHostCard());
|
||||
}
|
||||
|
||||
if (!mandatory && opp.cantLose()) {
|
||||
if (!mandatory && loser.cantLose()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
sa.getTargets().add(loser);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
56
forge-ai/src/main/java/forge/ai/ability/LearnAi.java
Normal file
56
forge-ai/src/main/java/forge/ai/ability/LearnAi.java
Normal file
@@ -0,0 +1,56 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class LearnAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
// For the time being, Learn is treated as universally positive due to being optional
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
|
||||
return mandatory || canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
return canPlayAI(aiPlayer, sa);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public static Card chooseCardToLearn(CardCollection options, Player ai, SpellAbility sa) {
|
||||
CardCollection sideboard = CardLists.filter(options, CardPredicates.inZone(ZoneType.Sideboard));
|
||||
CardCollection hand = CardLists.filter(options, CardPredicates.inZone(ZoneType.Hand));
|
||||
hand.remove(sa.getHostCard()); // this card will be used in the process, don't consider it for discard
|
||||
|
||||
CardCollection lessons = CardLists.filter(sideboard, CardPredicates.isType("Lesson"));
|
||||
CardCollection goodDiscards = ((PlayerControllerAi)ai.getController()).getAi().getCardsToDiscard(1, 1, hand, sa);
|
||||
|
||||
if (!lessons.isEmpty()) {
|
||||
return ComputerUtilCard.getBestAI(lessons);
|
||||
} else if (!goodDiscards.isEmpty()) {
|
||||
return ComputerUtilCard.getWorstAI(goodDiscards);
|
||||
}
|
||||
|
||||
// Don't choose anything if there's no good option
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
@@ -19,7 +20,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
|
||||
final int myLife = aiPlayer.getLife();
|
||||
Player opponent = aiPlayer.getWeakestOpponent();
|
||||
Player opponent = AiAttackController.choosePreferredDefenderPlayer(aiPlayer);
|
||||
final int hLife = opponent.getLife();
|
||||
|
||||
if (!aiPlayer.canGainLife()) {
|
||||
@@ -75,7 +76,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
|
||||
final boolean mandatory) {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiProps;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
@@ -154,7 +155,7 @@ public class LifeExchangeVariantAi extends SpellAbilityAi {
|
||||
final boolean mandatory) {
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {
|
||||
|
||||
@@ -266,8 +266,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
}
|
||||
if (!hasTgt && mandatory) {
|
||||
// need to target something but its neither negative against
|
||||
// opponents,
|
||||
// nor posive against allies
|
||||
// opponents, nor positive against allies
|
||||
|
||||
// hurting ally is probably better than healing opponent
|
||||
// look for Lifegain not Negative (case of lifegain negated)
|
||||
@@ -295,8 +294,7 @@ public class LifeGainAi extends SpellAbilityAi {
|
||||
sa.getTargets().add(ally);
|
||||
hasTgt = true;
|
||||
}
|
||||
// better heal opponent which most life then the one with the
|
||||
// lowest
|
||||
// better heal opponent which most life then the one with the lowest
|
||||
if (!hasTgt) {
|
||||
Player opp = opps.max(PlayerPredicates.compareByLife());
|
||||
sa.getTargets().add(opp);
|
||||
|
||||
@@ -28,7 +28,6 @@ public class LifeLoseAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
|
||||
|
||||
final PlayerCollection tgtPlayers = getPlayers(ai, sa);
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
@@ -60,7 +59,6 @@ public class LifeLoseAi extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* (non-Javadoc)
|
||||
*
|
||||
@@ -123,12 +121,12 @@ public class LifeLoseAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
final PlayerCollection tgtPlayers = getPlayers(ai, sa);
|
||||
|
||||
if (ComputerUtil.playImmediately(ai, sa)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final PlayerCollection tgtPlayers = getPlayers(ai, sa);
|
||||
// TODO: check against the amount we could obtain when multiple activations are possible
|
||||
PlayerCollection filteredPlayer = tgtPlayers
|
||||
.filter(Predicates.and(PlayerPredicates.isOpponentOf(ai), PlayerPredicates.lifeLessOrEqualTo(amount)));
|
||||
// killing opponents asap
|
||||
|
||||
@@ -17,7 +17,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final int myLife = ai.getLife();
|
||||
final Player opponent = ai.getWeakestOpponent();
|
||||
final Player opponent = ai.getStrongestOpponent();
|
||||
final int hlife = opponent.getLife();
|
||||
final String amountStr = sa.getParam("LifeAmount");
|
||||
|
||||
@@ -36,8 +36,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO handle proper calculation of X values based on Cost and what
|
||||
// would be paid
|
||||
// TODO handle proper calculation of X values based on Cost and what would be paid
|
||||
int amount;
|
||||
// we shouldn't have to worry too much about PayX for SetLife
|
||||
if (amountStr.equals("X") && sa.getSVar(amountStr).equals("Count$xPaid")) {
|
||||
@@ -58,11 +57,9 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
if (tgt.canOnlyTgtOpponent()) {
|
||||
sa.getTargets().add(opponent);
|
||||
// if we can only target the human, and the Human's life
|
||||
// would
|
||||
// go up, don't play it.
|
||||
// would go up, don't play it.
|
||||
// possibly add a combo here for Magister Sphinx and
|
||||
// Higedetsu's
|
||||
// (sp?) Second Rite
|
||||
// Higedetsu's (sp?) Second Rite
|
||||
if ((amount > hlife) || !opponent.canLoseLife()) {
|
||||
return false;
|
||||
}
|
||||
@@ -81,8 +78,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
if (sa.getParam("Defined").equals("Player")) {
|
||||
if (amount == 0) {
|
||||
return false;
|
||||
} else if (myLife > amount) { // will decrease computer's
|
||||
// life
|
||||
} else if (myLife > amount) { // will decrease computer's life
|
||||
if ((myLife < 5) || ((myLife - amount) > (hlife - amount))) {
|
||||
return false;
|
||||
}
|
||||
@@ -104,7 +100,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final int myLife = ai.getLife();
|
||||
final Player opponent = ai.getWeakestOpponent();
|
||||
final Player opponent = ai.getStrongestOpponent();
|
||||
final int hlife = opponent.getLife();
|
||||
final Card source = sa.getHostCard();
|
||||
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
|
||||
@@ -133,8 +129,7 @@ public class LifeSetAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// If the Target is gaining life, target self.
|
||||
// if the Target is modifying how much life is gained, this needs to
|
||||
// be handled better
|
||||
// if the Target is modifying how much life is gained, this needs to be handled better
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -21,6 +21,7 @@ import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.player.PlayerPredicates;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -214,13 +215,7 @@ public class MillAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// get targeted or defined Player with largest library
|
||||
// TODO in Java 8 find better way
|
||||
final Player m = Collections.max(list, new Comparator<Player>() {
|
||||
@Override
|
||||
public int compare(Player arg0, Player arg1) {
|
||||
return arg0.getCardsIn(ZoneType.Library).size() - arg1.getCardsIn(ZoneType.Library).size();
|
||||
}
|
||||
});
|
||||
final Player m = Collections.max(list, PlayerPredicates.compareByZoneSize(ZoneType.Library));
|
||||
|
||||
int cardsToDiscard = m.getCardsIn(ZoneType.Library).size();
|
||||
|
||||
|
||||
@@ -297,7 +297,7 @@ public class PermanentAi extends SpellAbilityAi {
|
||||
if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
|
||||
return false;
|
||||
}
|
||||
return checkApiLogic(ai, sa);
|
||||
return checkApiLogic(ai, sa) || mandatory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
/**
|
||||
@@ -40,10 +39,8 @@ public class PermanentNoncreatureAi extends PermanentAi {
|
||||
if (host.hasSVar("OblivionRing")) {
|
||||
SpellAbility effectExile = AbilityFactory.getAbility(host.getSVar("TrigExile"), host);
|
||||
final ZoneType origin = ZoneType.listValueOf(effectExile.getParam("Origin")).get(0);
|
||||
final TargetRestrictions tgt = effectExile.getTargetRestrictions();
|
||||
final CardCollection list = CardLists.getValidCards(game.getCardsIn(origin), tgt.getValidTgts(), ai, host,
|
||||
effectExile);
|
||||
CardCollection targets = CardLists.getTargetableCards(list, sa);
|
||||
effectExile.setActivatingPlayer(ai);
|
||||
CardCollection targets = CardLists.getTargetableCards(game.getCardsIn(origin), effectExile);
|
||||
if (sourceName.equals("Suspension Field")
|
||||
|| sourceName.equals("Detention Sphere")) {
|
||||
// existing "exile until leaves" enchantments only target
|
||||
|
||||
@@ -28,7 +28,6 @@ import forge.game.card.CardPredicates;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.keyword.Keyword;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerActionConfirmMode;
|
||||
import forge.game.spellability.Spell;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.spellability.SpellAbilityPredicates;
|
||||
@@ -158,16 +157,7 @@ public class PlayAi extends SpellAbilityAi {
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
|
||||
*/
|
||||
@Override
|
||||
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
|
||||
// as called from PlayEffect:173
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
/* (non-Javadoc)
|
||||
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
|
||||
*/
|
||||
@@ -202,7 +192,7 @@ public class PlayAi extends SpellAbilityAi {
|
||||
|
||||
spell = (Spell) spell.copyWithDefinedCost(abCost);
|
||||
}
|
||||
if( AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
|
||||
if (AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlayFromEffectAI(spell, !isOptional, true)) {
|
||||
// Before accepting, see if the spell has a valid number of targets (it should at this point).
|
||||
// Proceeding past this point if the spell is not correctly targeted will result
|
||||
// in "Failed to add to stack" error and the card disappearing from the game completely.
|
||||
|
||||
@@ -30,13 +30,12 @@ public class PowerExchangeAi extends SpellAbilityAi {
|
||||
sa.resetTargets();
|
||||
|
||||
List<Card> list =
|
||||
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on
|
||||
// purpose
|
||||
CardLists.getValidCards(ai.getGame().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
|
||||
// AI won't try to grab cards that are filtered out of AI decks on purpose
|
||||
list = CardLists.filter(list, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa);
|
||||
return !ComputerUtilCard.isCardRemAIDeck(c) && c.canBeTargetedBy(sa) && c.getController() != ai;
|
||||
}
|
||||
});
|
||||
CardLists.sortByPowerAsc(list);
|
||||
|
||||
@@ -400,7 +400,6 @@ public class PumpAi extends PumpAiBase {
|
||||
if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
|
||||
return true;
|
||||
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
|
||||
|
||||
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
|
||||
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS, ai)
|
||||
|| game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN, ai)) {
|
||||
@@ -438,7 +437,7 @@ public class PumpAi extends PumpAiBase {
|
||||
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
&& !(sa.isCurse() && defense < 0)
|
||||
&& !containsNonCombatKeyword(keywords)
|
||||
&& !sa.hasParam("UntilYourNextTurn")
|
||||
&& !"UntilYourNextTurn".equals(sa.getParam("Duration"))
|
||||
&& !"Snapcaster".equals(sa.getParam("AILogic"))
|
||||
&& !isFight) {
|
||||
return false;
|
||||
|
||||
@@ -6,6 +6,7 @@ import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCombat;
|
||||
@@ -43,7 +44,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
public boolean grantsUsefulExtraBlockOpts(final Player ai, final SpellAbility sa, final Card card, List<String> keywords) {
|
||||
PhaseHandler ph = ai.getGame().getPhaseHandler();
|
||||
Card pumped = ComputerUtilCard.getPumpedCreature(ai, sa, card, 0, 0, keywords);
|
||||
@@ -109,7 +109,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
&& (card.getNetCombatDamage() > 0)
|
||||
&& !ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
|
||||
} else if (keyword.endsWith("CARDNAME can't attack or block.")) {
|
||||
if (sa.hasParam("UntilYourNextTurn")) {
|
||||
if ("UntilYourNextTurn".equals(sa.getParam("Duration"))) {
|
||||
return CombatUtil.canAttack(card, ai) || CombatUtil.canBlock(card, true);
|
||||
}
|
||||
if (!ph.isPlayerTurn(ai)) {
|
||||
@@ -153,14 +153,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
}
|
||||
});
|
||||
return CombatUtil.canBlockAtLeastOne(card, attackers);
|
||||
} else if (keyword.endsWith("CantBlockCardUIDSource")) { // can't block CARDNAME this turn
|
||||
if (!ph.isPlayerTurn(ai) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
|| ph.getPhase().isBefore(PhaseType.MAIN1) || !CombatUtil.canBlock(sa.getHostCard(), card)) {
|
||||
return false;
|
||||
}
|
||||
// target needs to be a creature, controlled by the player which is attacked
|
||||
return !sa.getHostCard().isTapped() || (combat != null && combat.isAttacking(sa.getHostCard())
|
||||
&& card.getController().equals(combat.getDefenderPlayerByAttacker(sa.getHostCard())));
|
||||
} else if (keyword.endsWith("This card doesn't untap during your next untap step.")) {
|
||||
return !ph.getPhase().isBefore(PhaseType.MAIN2) && !card.isUntapped() && ph.isPlayerTurn(ai)
|
||||
&& Untap.canUntap(card);
|
||||
@@ -201,7 +193,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
final Game game = ai.getGame();
|
||||
final Combat combat = game.getCombat();
|
||||
final PhaseHandler ph = game.getPhaseHandler();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final int newPower = card.getNetCombatDamage() + attack;
|
||||
//int defense = getNumDefense(sa);
|
||||
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
|
||||
@@ -479,7 +471,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
}
|
||||
}); // leaves all creatures that will be destroyed
|
||||
} // -X/-X end
|
||||
else if (attack < 0 && !game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
|
||||
else if (attack < 0 && !game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
|
||||
// spells that give -X/0
|
||||
boolean isMyTurn = game.getPhaseHandler().isPlayerTurn(ai);
|
||||
if (isMyTurn) {
|
||||
@@ -513,7 +505,6 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
else {
|
||||
final boolean addsKeywords = !keywords.isEmpty();
|
||||
if (addsKeywords) {
|
||||
|
||||
// If the keyword can prevent a creature from attacking, see if there's some kind of viable prioritization
|
||||
if (keywords.contains("CARDNAME can't attack.") || keywords.contains("CARDNAME can't attack or block.")
|
||||
|| keywords.contains("HIDDEN CARDNAME can't attack.") || keywords.contains("HIDDEN CARDNAME can't attack or block.")) {
|
||||
@@ -539,8 +530,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|
||||
|
||||
protected boolean containsNonCombatKeyword(final List<String> keywords) {
|
||||
for (final String keyword : keywords) {
|
||||
// since most keywords are combat relevant check for those that are
|
||||
// not
|
||||
// since most keywords are combat relevant check for those that are not
|
||||
if (keyword.endsWith("This card doesn't untap during your next untap step.")
|
||||
|| keyword.endsWith("Shroud") || keyword.endsWith("Hexproof")) {
|
||||
return true;
|
||||
|
||||
@@ -48,12 +48,6 @@ public class PumpAllAi extends PumpAiBase {
|
||||
}
|
||||
}
|
||||
|
||||
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
|
||||
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
|
||||
final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>();
|
||||
|
||||
final PhaseType phase = game.getPhaseHandler().getPhase();
|
||||
|
||||
if (ComputerUtil.preventRunAwayActivations(sa)) {
|
||||
return false;
|
||||
}
|
||||
@@ -63,16 +57,10 @@ public class PumpAllAi extends PumpAiBase {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (sa.hasParam("ValidCards")) {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
|
||||
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
|
||||
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final Player opp = ai.getStrongestOpponent();
|
||||
|
||||
if (tgt != null && sa.canTarget(opp) && sa.hasParam("IsCurse")) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
@@ -85,6 +73,18 @@ public class PumpAllAi extends PumpAiBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
final int power = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumAtt"), sa);
|
||||
final int defense = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("NumDef"), sa);
|
||||
final List<String> keywords = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & ")) : new ArrayList<>();
|
||||
final PhaseType phase = game.getPhaseHandler().getPhase();
|
||||
|
||||
if (sa.hasParam("ValidCards")) {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
|
||||
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
|
||||
|
||||
if (!game.getStack().isEmpty() && !sa.isCurse()) {
|
||||
return pumpAgainstRemoval(ai, sa, comp);
|
||||
}
|
||||
@@ -113,7 +113,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
if (phase.isAfter(PhaseType.COMBAT_DECLARE_BLOCKERS)
|
||||
|| phase.isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)
|
||||
|| game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())
|
||||
|| game.getPhaseHandler().isPreventCombatDamageThisTurn()) {
|
||||
|| game.getReplacementHandler().isPreventCombatDamageThisTurn()) {
|
||||
return false;
|
||||
}
|
||||
int totalPower = 0;
|
||||
@@ -139,8 +139,7 @@ public class PumpAllAi extends PumpAiBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
// evaluate both lists and pass only if human creatures are more
|
||||
// valuable
|
||||
// evaluate both lists and pass only if human creatures are more valuable
|
||||
return (ComputerUtilCard.evaluateCreatureList(comp) + 200) < ComputerUtilCard.evaluateCreatureList(human);
|
||||
} // end Curse
|
||||
|
||||
@@ -152,6 +151,12 @@ public class PumpAllAi extends PumpAiBase {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
// important to call canPlay first so targets are added if needed
|
||||
return canPlayAI(ai, sa) || mandatory;
|
||||
}
|
||||
|
||||
boolean pumpAgainstRemoval(Player ai, SpellAbility sa, List<Card> comp) {
|
||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
|
||||
for (final Card c : comp) {
|
||||
|
||||
@@ -49,14 +49,6 @@ import forge.game.zone.ZoneType;
|
||||
*/
|
||||
public class RegenerateAi extends SpellAbilityAi {
|
||||
|
||||
// Ex: A:SP$Regenerate | Cost$W | ValidTgts$ Creature | TgtPrompt$ Select target creature | SpellDescription$Regenerate
|
||||
// target creature.
|
||||
// http://www.slightlymagic.net/wiki/Forge_AbilityFactory#Regenerate
|
||||
|
||||
// **************************************************************
|
||||
// ********************* Regenerate ****************************
|
||||
// **************************************************************
|
||||
|
||||
@Override
|
||||
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
|
||||
final Game game = ai.getGame();
|
||||
@@ -65,8 +57,7 @@ public class RegenerateAi extends SpellAbilityAi {
|
||||
boolean chance = false;
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt == null) {
|
||||
// As far as I can tell these Defined Cards will only have one of
|
||||
// them
|
||||
// As far as I can tell these Defined Cards will only have one of them
|
||||
final List<Card> list = AbilityUtils.getDefinedCards(hostCard, sa.getParam("Defined"), sa);
|
||||
|
||||
if (!game.getStack().isEmpty()) {
|
||||
@@ -105,8 +96,7 @@ public class RegenerateAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
if (!game.getStack().isEmpty()) {
|
||||
// check stack for something on the stack will kill anything i
|
||||
// control
|
||||
// check stack for something on the stack will kill anything i control
|
||||
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
|
||||
|
||||
final List<Card> threatenedTargets = new ArrayList<>();
|
||||
@@ -191,8 +181,7 @@ public class RegenerateAi extends SpellAbilityAi {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO see if something on the stack is about to kill something i
|
||||
// can target
|
||||
// TODO see if something on the stack is about to kill something i can target
|
||||
|
||||
// choose my best X without regen
|
||||
if (CardLists.getNotType(compTargetables, "Creature").isEmpty()) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package forge.ai.ability;
|
||||
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.AiController;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.PlayerControllerAi;
|
||||
@@ -14,10 +15,10 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
if (!opp.canBeTargetedBy(sa)) {
|
||||
if (!sa.canTarget(opp)) {
|
||||
return false;
|
||||
}
|
||||
sa.resetTargets();
|
||||
@@ -44,9 +45,8 @@ public class RepeatAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
|
||||
if (sa.usesTargeting()) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
if (sa.canTarget(opp)) {
|
||||
sa.resetTargets();
|
||||
sa.getTargets().add(opp);
|
||||
|
||||
@@ -16,7 +16,6 @@ import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardUtil;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.TextUtil;
|
||||
@@ -67,7 +66,7 @@ public class RepeatEachAi extends SpellAbilityAi {
|
||||
return false;
|
||||
} else if ("AllPlayerLoseLife".equals(logic)) {
|
||||
final Card source = sa.getHostCard();
|
||||
AbilitySub repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||
SpellAbility repeat = sa.getAdditionalAbility("RepeatSubAbility");
|
||||
|
||||
String svar = repeat.getSVar(repeat.getParam("LifeAmount"));
|
||||
// replace RememberedPlayerCtrl with YouCtrl
|
||||
|
||||
@@ -20,9 +20,6 @@ import forge.game.spellability.TargetRestrictions;
|
||||
import forge.game.zone.ZoneType;
|
||||
|
||||
public class SacrificeAi extends SpellAbilityAi {
|
||||
// **************************************************************
|
||||
// *************************** Sacrifice ***********************
|
||||
// **************************************************************
|
||||
|
||||
@Override
|
||||
protected boolean canPlayAI(Player ai, SpellAbility sa) {
|
||||
@@ -48,8 +45,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
// Improve AI for triggers. If source is a creature with:
|
||||
// When ETB, sacrifice a creature. Check to see if the AI has something
|
||||
// to sacrifice
|
||||
// When ETB, sacrifice a creature. Check to see if the AI has something to sacrifice
|
||||
|
||||
// Eventually, we can call the trigger of ETB abilities with not
|
||||
// mandatory as part of the checks to cast something
|
||||
@@ -58,12 +54,11 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
private boolean sacrificeTgtAI(final Player ai, final SpellAbility sa) {
|
||||
|
||||
final Card source = sa.getHostCard();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
final boolean destroy = sa.hasParam("Destroy");
|
||||
|
||||
Player opp = ai.getWeakestOpponent();
|
||||
Player opp = ai.getStrongestOpponent();
|
||||
|
||||
if (tgt != null) {
|
||||
sa.resetTargets();
|
||||
@@ -109,8 +104,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
sa.setXManaCostPaid(Math.min(ComputerUtilCost.getMaxXValue(sa, ai), amount));
|
||||
}
|
||||
|
||||
final int half = (amount / 2) + (amount % 2); // Half of amount
|
||||
// rounded up
|
||||
final int half = (amount / 2) + (amount % 2); // Half of amount rounded up
|
||||
|
||||
// If the Human has at least half rounded up of the amount to be
|
||||
// sacrificed, cast the spell
|
||||
@@ -130,8 +124,7 @@ public class SacrificeAi extends SpellAbilityAi {
|
||||
// If Sacrifice hits both players:
|
||||
// Only cast it if Human has the full amount of valid
|
||||
// Only cast it if AI doesn't have the full amount of Valid
|
||||
// TODO: Cast if the type is favorable: my "worst" valid is
|
||||
// worse than his "worst" valid
|
||||
// TODO: Cast if the type is favorable: my "worst" valid is worse than his "worst" valid
|
||||
final String num = sa.hasParam("Amount") ? sa.getParam("Amount") : "1";
|
||||
int amount = AbilityUtils.calculateAmount(source, num, sa);
|
||||
|
||||
|
||||
@@ -2,17 +2,12 @@ package forge.ai.ability;
|
||||
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.cost.Cost;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
import forge.util.MyRandom;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
public class SacrificeAllAi extends SpellAbilityAi {
|
||||
|
||||
@@ -22,22 +17,7 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
||||
// based on what the expected targets could be
|
||||
final Cost abCost = sa.getPayCosts();
|
||||
final Card source = sa.getHostCard();
|
||||
String valid = "";
|
||||
|
||||
if (sa.hasParam("ValidCards")) {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
if (valid.contains("X") && sa.getSVar("X").equals("Count$xPaid")) {
|
||||
// Set PayX here to maximum value.
|
||||
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
|
||||
valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay));
|
||||
}
|
||||
|
||||
CardCollection humanlist =
|
||||
CardLists.getValidCards(ai.getWeakestOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
CardCollection computerlist =
|
||||
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
|
||||
final String logic = sa.getParamOrDefault("AILogic", "");
|
||||
|
||||
if (abCost != null) {
|
||||
// AI currently disabled for some costs
|
||||
@@ -45,30 +25,20 @@ public class SacrificeAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (logic.equals("HellionEruption")) {
|
||||
if (ai.getCreaturesInPlay().size() < 5 || ai.getCreaturesInPlay().size() * 150 < ComputerUtilCard.evaluateCreatureList(ai.getCreaturesInPlay())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!DestroyAllAi.doMassRemovalLogic(ai, sa)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// prevent run-away activations - first time will always return true
|
||||
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
|
||||
|
||||
// if only creatures are affected evaluate both lists and pass only if
|
||||
// human creatures are more valuable
|
||||
if ((CardLists.getNotType(humanlist, "Creature").size() == 0) && (CardLists.getNotType(computerlist, "Creature").size() == 0)) {
|
||||
if ((ComputerUtilCard.evaluateCreatureList(computerlist) + 200) >= ComputerUtilCard
|
||||
.evaluateCreatureList(humanlist)) {
|
||||
return false;
|
||||
}
|
||||
} // only lands involved
|
||||
else if ((CardLists.getNotType(humanlist, "Land").size() == 0) && (CardLists.getNotType(computerlist, "Land").size() == 0)) {
|
||||
if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 1) >= ComputerUtilCard
|
||||
.evaluatePermanentList(humanlist)) {
|
||||
return false;
|
||||
}
|
||||
} // otherwise evaluate both lists by CMC and pass only if human
|
||||
// permanents are more valuable
|
||||
else if ((ComputerUtilCard.evaluatePermanentList(computerlist) + 3) >= ComputerUtilCard
|
||||
.evaluatePermanentList(humanlist)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return ((MyRandom.getRandom().nextFloat() < .9667) && chance);
|
||||
}
|
||||
|
||||
|
||||
@@ -3,14 +3,12 @@ package forge.ai.ability;
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
import forge.ai.ComputerUtilMana;
|
||||
import forge.ai.SpecialCardAi;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.Card.SplitCMCMode;
|
||||
import forge.game.card.CardCollection;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.card.CounterEnumType;
|
||||
import forge.game.phase.PhaseHandler;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -47,6 +45,11 @@ public class ScryAi extends SpellAbilityAi {
|
||||
*/
|
||||
@Override
|
||||
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
|
||||
// For Brain in a Jar, avoid competing against the other ability in the opponent's EOT.
|
||||
if ("BrainJar".equals(sa.getParam("AILogic"))) {
|
||||
return ph.getPhase().isAfter(PhaseType.MAIN2);
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -102,56 +105,9 @@ public class ScryAi extends SpellAbilityAi {
|
||||
if ("Never".equals(aiLogic)) {
|
||||
return false;
|
||||
} else if ("BrainJar".equals(aiLogic)) {
|
||||
final Card source = sa.getHostCard();
|
||||
|
||||
int counterNum = source.getCounters(CounterEnumType.CHARGE);
|
||||
// no need for logic
|
||||
if (counterNum == 0) {
|
||||
return false;
|
||||
}
|
||||
int libsize = ai.getCardsIn(ZoneType.Library).size();
|
||||
|
||||
final CardCollection hand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), Predicates.or(
|
||||
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
|
||||
if (!hand.isEmpty()) {
|
||||
// has spell that can be cast in hand with put ability
|
||||
if (!CardLists.filter(hand, CardPredicates.hasCMC(counterNum + 1)).isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// has spell that can be cast if one counter is removed
|
||||
if (!CardLists.filter(hand, CardPredicates.hasCMC(counterNum)).isEmpty()) {
|
||||
sa.setXManaCostPaid(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
final CardCollection library = CardLists.filter(ai.getCardsIn(ZoneType.Library), Predicates.or(
|
||||
CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
|
||||
if (!library.isEmpty()) {
|
||||
// get max cmc of instant or sorceries in the libary
|
||||
int maxCMC = 0;
|
||||
for (final Card c : library) {
|
||||
int v = c.getCMC();
|
||||
if (c.isSplitCard()) {
|
||||
v = Math.max(c.getCMC(SplitCMCMode.LeftSplitCMC), c.getCMC(SplitCMCMode.RightSplitCMC));
|
||||
}
|
||||
if (v > maxCMC) {
|
||||
maxCMC = v;
|
||||
}
|
||||
}
|
||||
// there is a spell with more CMC, no need to remove counter
|
||||
if (counterNum + 1 < maxCMC) {
|
||||
return false;
|
||||
}
|
||||
int maxToRemove = counterNum - maxCMC + 1;
|
||||
// no Scry 0, even if its catched from later stuff
|
||||
if (maxToRemove <= 0) {
|
||||
return false;
|
||||
}
|
||||
sa.setXManaCostPaid(maxToRemove);
|
||||
} else {
|
||||
// no Instant or Sorceries anymore, just scry
|
||||
sa.setXManaCostPaid(Math.min(counterNum, libsize));
|
||||
}
|
||||
return SpecialCardAi.BrainInAJar.consider(ai, sa);
|
||||
} else if ("MultipleChoice".equals(aiLogic)) {
|
||||
return SpecialCardAi.MultipleChoice.consider(ai, sa);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -163,11 +119,9 @@ public class ScryAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
double chance = .4; // 40 percent chance of milling with instant speed
|
||||
// stuff
|
||||
double chance = .4; // 40 percent chance of milling with instant speed stuff
|
||||
if (SpellAbilityAi.isSorcerySpeed(sa)) {
|
||||
chance = .667; // 66.7% chance for sorcery speed (since it will
|
||||
// never activate EOT)
|
||||
chance = .667; // 66.7% chance for sorcery speed (since it will never activate EOT)
|
||||
}
|
||||
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
|
||||
|
||||
|
||||
@@ -52,8 +52,7 @@ public class SetStateAi extends SpellAbilityAi {
|
||||
|
||||
@Override
|
||||
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
|
||||
// Gross generalization, but this always considers alternate
|
||||
// states more powerful
|
||||
// Gross generalization, but this always considers alternate states more powerful
|
||||
return !sa.getHostCard().isInAlternateState();
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +17,7 @@ public class ShuffleAi extends SpellAbilityAi {
|
||||
return aiPlayer.getGame().getPhaseHandler().is(PhaseType.MAIN2, aiPlayer);
|
||||
}
|
||||
|
||||
// not really sure when the compy would use this; maybe only after a
|
||||
// human
|
||||
// not really sure when the compy would use this; maybe only after a human
|
||||
// deliberately put a card on top of their library
|
||||
return false;
|
||||
/*
|
||||
|
||||
@@ -66,7 +66,6 @@ public class TapAi extends TapAiBase {
|
||||
// Set PayX here to maximum value.
|
||||
// TODO need to set XManaCostPaid for targets, maybe doesn't need PayX anymore?
|
||||
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai));
|
||||
// TODO since change of PayX. the shouldCastLessThanMax logic might be faulty
|
||||
}
|
||||
|
||||
sa.resetTargets();
|
||||
|
||||
@@ -5,11 +5,13 @@ import java.util.List;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.collect.Iterables;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.ability.ApiType;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollection;
|
||||
@@ -109,7 +111,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
* @return a boolean.
|
||||
*/
|
||||
protected boolean tapPrefTargeting(final Player ai, final Card source, final SpellAbility sa, final boolean mandatory) {
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
final Game game = ai.getGame();
|
||||
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
|
||||
tapList = CardLists.getTargetableCards(tapList, sa);
|
||||
@@ -308,9 +310,8 @@ public abstract class TapAiBase extends SpellAbilityAi {
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: use Defined to determine, if this is an unfavorable result
|
||||
|
||||
return true;
|
||||
final List<Card> pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
return pDefined.isEmpty() || (pDefined.get(0).isUntapped() && pDefined.get(0).getController() != ai);
|
||||
} else {
|
||||
sa.resetTargets();
|
||||
if (tapPrefTargeting(ai, source, sa, mandatory)) {
|
||||
|
||||
@@ -12,7 +12,7 @@ import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
import forge.game.card.CardCollectionView;
|
||||
import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates.Presets;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.combat.CombatUtil;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
@@ -49,7 +49,7 @@ public class TapAllAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
validTappables = CardLists.getValidCards(validTappables, valid, source.getController(), source, sa);
|
||||
validTappables = CardLists.filter(validTappables, Presets.UNTAPPED);
|
||||
validTappables = CardLists.filter(validTappables, CardPredicates.Presets.UNTAPPED);
|
||||
|
||||
if (sa.hasParam("AILogic")) {
|
||||
String logic = sa.getParam("AILogic");
|
||||
@@ -69,18 +69,8 @@ public class TapAllAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
final List<Card> human = CardLists.filter(validTappables, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.getController().equals(opp);
|
||||
}
|
||||
});
|
||||
final List<Card> compy = CardLists.filter(validTappables, new Predicate<Card>() {
|
||||
@Override
|
||||
public boolean apply(final Card c) {
|
||||
return c.getController().equals(ai);
|
||||
}
|
||||
});
|
||||
final List<Card> human = CardLists.filterControlledBy(validTappables, opp);
|
||||
final List<Card> compy = CardLists.filterControlledBy(validTappables, ai);
|
||||
if (human.size() <= compy.size()) {
|
||||
return false;
|
||||
}
|
||||
@@ -102,7 +92,7 @@ public class TapAllAi extends SpellAbilityAi {
|
||||
final Game game = source.getGame();
|
||||
CardCollectionView tmpList = game.getCardsIn(ZoneType.Battlefield);
|
||||
tmpList = CardLists.getValidCards(tmpList, valid, source.getController(), source, sa);
|
||||
tmpList = CardLists.filter(tmpList, Presets.UNTAPPED);
|
||||
tmpList = CardLists.filter(tmpList, CardPredicates.Presets.UNTAPPED);
|
||||
return tmpList;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,7 @@ public class TapOrUntapAi extends TapAiBase {
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
// assume we are looking to tap human's stuff
|
||||
// TODO - check for things with untap abilities, and don't tap
|
||||
// those.
|
||||
// TODO - check for things with untap abilities, and don't tap those.
|
||||
|
||||
boolean bFlag = false;
|
||||
for (final Card c : AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa)) {
|
||||
@@ -40,6 +39,4 @@ public class TapOrUntapAi extends TapAiBase {
|
||||
return randomReturn;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package forge.ai.ability;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.SpellAbilityAi;
|
||||
import forge.game.ability.AbilityUtils;
|
||||
import forge.game.card.Card;
|
||||
@@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
if (tgt != null) {
|
||||
|
||||
@@ -56,7 +56,6 @@ public class UnattachAllAi extends SpellAbilityAi {
|
||||
@Override
|
||||
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
|
||||
final Card card = sa.getHostCard();
|
||||
final Player opp = ai.getWeakestOpponent();
|
||||
// Check if there are any valid targets
|
||||
List<GameObject> targets = new ArrayList<>();
|
||||
final TargetRestrictions tgt = sa.getTargetRestrictions();
|
||||
@@ -66,8 +65,8 @@ public class UnattachAllAi extends SpellAbilityAi {
|
||||
|
||||
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
|
||||
Card newTarget = (Card) targets.get(0);
|
||||
//don't equip human creatures
|
||||
if (newTarget.getController().equals(opp)) {
|
||||
//don't equip opponent creatures
|
||||
if (!newTarget.getController().equals(ai)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import java.util.Map;
|
||||
import com.google.common.base.Predicate;
|
||||
import com.google.common.base.Predicates;
|
||||
|
||||
import forge.ai.AiAttackController;
|
||||
import forge.ai.ComputerUtil;
|
||||
import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCard;
|
||||
@@ -65,7 +66,7 @@ public class UntapAi extends SpellAbilityAi {
|
||||
|
||||
if (!sa.usesTargeting()) {
|
||||
final List<Card> pDefined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
|
||||
return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai;
|
||||
return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
|
||||
} else {
|
||||
return untapPrefTargeting(ai, sa, false);
|
||||
}
|
||||
@@ -81,9 +82,8 @@ public class UntapAi extends SpellAbilityAi {
|
||||
return false;
|
||||
}
|
||||
|
||||
// TODO: use Defined to determine, if this is an unfavorable result
|
||||
final List<Card> pDefined = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
|
||||
return pDefined == null || !pDefined.get(0).isUntapped() || pDefined.get(0).getController() != ai;
|
||||
return pDefined.isEmpty() || (pDefined.get(0).isTapped() && pDefined.get(0).getController() == ai);
|
||||
} else {
|
||||
if (untapPrefTargeting(ai, sa, mandatory)) {
|
||||
return true;
|
||||
@@ -130,7 +130,8 @@ public class UntapAi extends SpellAbilityAi {
|
||||
Player targetController = ai;
|
||||
|
||||
if (sa.isCurse()) {
|
||||
targetController = ai.getWeakestOpponent();
|
||||
// TODO search through all opponents, may need to check if different controllers allowed
|
||||
targetController = AiAttackController.choosePreferredDefenderPlayer(ai);
|
||||
}
|
||||
|
||||
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
|
||||
@@ -149,8 +150,7 @@ public class UntapAi extends SpellAbilityAi {
|
||||
}
|
||||
|
||||
CardCollection untapList = targetUntapped ? list : CardLists.filter(list, Presets.TAPPED);
|
||||
// filter out enchantments and planeswalkers, their tapped state doesn't
|
||||
// matter.
|
||||
// filter out enchantments and planeswalkers, their tapped state doesn't matter.
|
||||
final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
|
||||
untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa);
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import forge.game.card.CardLists;
|
||||
import forge.game.card.CardPredicates;
|
||||
import forge.game.phase.PhaseType;
|
||||
import forge.game.player.Player;
|
||||
import forge.game.player.PlayerCollection;
|
||||
import forge.game.spellability.AbilitySub;
|
||||
import forge.game.spellability.SpellAbility;
|
||||
import forge.game.zone.ZoneType;
|
||||
@@ -29,6 +30,10 @@ public class UntapAllAi extends SpellAbilityAi {
|
||||
valid = sa.getParam("ValidCards");
|
||||
}
|
||||
list = CardLists.getValidCards(list, valid.split(","), source.getController(), source, sa);
|
||||
// don't untap if only opponent benefits
|
||||
PlayerCollection goodControllers = aiPlayer.getAllies();
|
||||
goodControllers.add(aiPlayer);
|
||||
list = CardLists.filter(list, CardPredicates.isControlledByAnyOf(goodControllers));
|
||||
return !list.isEmpty();
|
||||
}
|
||||
return false;
|
||||
|
||||
@@ -55,15 +55,15 @@ public class GameCopier {
|
||||
public GameCopier(Game origGame) {
|
||||
this.origGame = origGame;
|
||||
}
|
||||
|
||||
|
||||
public Game getOriginalGame() {
|
||||
return origGame;
|
||||
}
|
||||
|
||||
|
||||
public Game getCopiedGame() {
|
||||
return gameObjectMap.getGame();
|
||||
}
|
||||
|
||||
|
||||
public Game makeCopy() {
|
||||
return makeCopy(null);
|
||||
}
|
||||
@@ -88,7 +88,6 @@ public class GameCopier {
|
||||
newPlayer.setCounters(Maps.newHashMap(origPlayer.getCounters()));
|
||||
newPlayer.setLifeLostLastTurn(origPlayer.getLifeLostLastTurn());
|
||||
newPlayer.setLifeLostThisTurn(origPlayer.getLifeLostThisTurn());
|
||||
newPlayer.setPreventNextDamage(origPlayer.getPreventNextDamage());
|
||||
newPlayer.getManaPool().add(origPlayer.getManaPool());
|
||||
newPlayer.setCommanders(origPlayer.getCommanders()); // will be fixed up below
|
||||
playerMap.put(origPlayer, newPlayer);
|
||||
@@ -101,7 +100,7 @@ public class GameCopier {
|
||||
for (Player p : newGame.getPlayers()) {
|
||||
((PlayerZoneBattlefield) p.getZone(ZoneType.Battlefield)).setTriggers(false);
|
||||
}
|
||||
|
||||
|
||||
copyGameState(newGame);
|
||||
|
||||
for (Player p : newGame.getPlayers()) {
|
||||
@@ -124,6 +123,7 @@ public class GameCopier {
|
||||
System.err.println(c + " Remembered: " + o + "/" + o.getClass());
|
||||
c.addRemembered(o);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
for (SpellAbility sa : c.getSpellAbilities()) {
|
||||
@@ -152,7 +152,7 @@ public class GameCopier {
|
||||
if (advanceToPhase != null) {
|
||||
newGame.getPhaseHandler().devAdvanceToPhase(advanceToPhase);
|
||||
}
|
||||
|
||||
|
||||
return newGame;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ public class GameCopier {
|
||||
}
|
||||
newGame.getStack().add(newSa);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private RegisteredPlayer clonePlayer(RegisteredPlayer p) {
|
||||
@@ -226,7 +226,7 @@ public class GameCopier {
|
||||
// TODO: Verify that the above relationships are preserved bi-directionally or not.
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static final boolean USE_FROM_PAPER_CARD = true;
|
||||
private Card createCardCopy(Game newGame, Player newOwner, Card c) {
|
||||
if (c.isToken() && !c.isEmblem()) {
|
||||
@@ -276,7 +276,7 @@ public class GameCopier {
|
||||
// TODO: Controllers' list with timestamps should be copied.
|
||||
zoneOwner = playerMap.get(c.getController());
|
||||
newCard.setController(zoneOwner, 0);
|
||||
|
||||
|
||||
int setPower = c.getSetPower();
|
||||
int setToughness = c.getSetToughness();
|
||||
if (setPower != Integer.MAX_VALUE || setToughness != Integer.MAX_VALUE) {
|
||||
@@ -285,7 +285,7 @@ public class GameCopier {
|
||||
}
|
||||
newCard.setPTBoost(c.getPTBoostTable());
|
||||
newCard.setDamage(c.getDamage());
|
||||
|
||||
|
||||
newCard.setChangedCardTypes(c.getChangedCardTypesMap());
|
||||
newCard.setChangedCardKeywords(c.getChangedCardKeywords());
|
||||
newCard.setChangedCardNames(c.getChangedCardNames());
|
||||
@@ -334,16 +334,15 @@ public class GameCopier {
|
||||
if (c.getChosenPlayer() != null) {
|
||||
newCard.setChosenPlayer(playerMap.get(c.getChosenPlayer()));
|
||||
}
|
||||
|
||||
SpellAbility first = c.getFirstSpellAbility();
|
||||
|
||||
if (first != null && first.hasChosenColor()) {
|
||||
newCard.setChosenColors(Lists.newArrayList(first.getChosenColors()), newCard.getFirstSpellAbility());
|
||||
if (!c.getChosenType().isEmpty()) {
|
||||
newCard.setChosenType(c.getChosenType());
|
||||
}
|
||||
if (first != null && first.hasChosenType()) {
|
||||
newCard.setChosenColors(Lists.newArrayList(first.getChosenType()), newCard.getFirstSpellAbility());
|
||||
if (!c.getChosenType2().isEmpty()) {
|
||||
newCard.setChosenType2(c.getChosenType2());
|
||||
}
|
||||
if (c.getChosenColors() != null) {
|
||||
newCard.setChosenColors(Lists.newArrayList(c.getChosenColors()));
|
||||
}
|
||||
|
||||
if (!c.getNamedCard().isEmpty()) {
|
||||
newCard.setNamedCard(c.getNamedCard());
|
||||
}
|
||||
@@ -359,7 +358,7 @@ public class GameCopier {
|
||||
zoneOwner.getZone(zone).add(newCard);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static SpellAbility findSAInCard(SpellAbility sa, Card c) {
|
||||
String saDesc = sa.getDescription();
|
||||
for (SpellAbility cardSa : c.getAllSpellAbilities()) {
|
||||
@@ -387,7 +386,7 @@ public class GameCopier {
|
||||
return find(o);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public GameObject find(GameObject o) {
|
||||
GameObject result = cardMap.get(o);
|
||||
if (result != null)
|
||||
|
||||
@@ -11,6 +11,7 @@ import forge.ai.ComputerUtilAbility;
|
||||
import forge.ai.ComputerUtilCost;
|
||||
import forge.ai.ability.ChangeZoneAi;
|
||||
import forge.ai.ability.ExploreAi;
|
||||
import forge.ai.ability.LearnAi;
|
||||
import forge.ai.simulation.GameStateEvaluator.Score;
|
||||
import forge.game.Game;
|
||||
import forge.game.ability.ApiType;
|
||||
@@ -423,6 +424,8 @@ public class SpellAbilityPicker {
|
||||
}
|
||||
if (sa.getApi() == ApiType.Explore) {
|
||||
return ExploreAi.shouldPutInGraveyard(fetchList, decider);
|
||||
} else if (sa.getApi() == ApiType.Learn) {
|
||||
return LearnAi.chooseCardToLearn(fetchList, decider, sa);
|
||||
} else {
|
||||
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<artifactId>forge</artifactId>
|
||||
<groupId>forge</groupId>
|
||||
<version>1.6.40-SNAPSHOT</version>
|
||||
<version>1.6.41</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>forge-core</artifactId>
|
||||
|
||||
@@ -121,6 +121,9 @@ public final class ImageKeys {
|
||||
// if there's a 1st art variant try without it for .fullborder images
|
||||
file = findFile(dir, TextUtil.fastReplace(fullborderFile, "1.fullborder", ".fullborder"));
|
||||
if (file != null) { return file; }
|
||||
// if there's a 1st art variant try with it for .fullborder images
|
||||
file = findFile(dir, fullborderFile.replaceAll("[0-9]*.fullborder", "1.fullborder"));
|
||||
if (file != null) { return file; }
|
||||
// if there's an art variant try without it for .full images
|
||||
file = findFile(dir, filename.replaceAll("[0-9].full",".full"));
|
||||
if (file != null) { return file; }
|
||||
|
||||
@@ -2,7 +2,6 @@ package forge;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
@@ -79,16 +78,30 @@ public class StaticData {
|
||||
this.customEditions = new CardEdition.Collection(new CardEdition.Reader(new File(customEditionsFolder)));
|
||||
this.prefferedArt = prefferedArt;
|
||||
lastInstance = this;
|
||||
List<String> funnyCards = new ArrayList<>();
|
||||
List<String> filtered = new ArrayList<>();
|
||||
|
||||
{
|
||||
final Map<String, CardRules> regularCards = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
final Map<String, CardRules> variantsCards = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
final Map<String, CardRules> customizedCards = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
|
||||
|
||||
for (CardEdition e : editions) {
|
||||
if (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER) {
|
||||
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
|
||||
funnyCards.add(cis.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (CardRules card : cardReader.loadCards()) {
|
||||
if (null == card) continue;
|
||||
|
||||
final String cardName = card.getName();
|
||||
|
||||
if (!loadNonLegalCards && !card.getType().isBasicLand() && funnyCards.contains(cardName))
|
||||
filtered.add(cardName);
|
||||
|
||||
if (card.isVariant()) {
|
||||
variantsCards.put(cardName, card);
|
||||
} else {
|
||||
@@ -104,15 +117,18 @@ public class StaticData {
|
||||
}
|
||||
}
|
||||
|
||||
if (!filtered.isEmpty()) {
|
||||
Collections.sort(filtered);
|
||||
}
|
||||
|
||||
commonCards = new CardDb(regularCards, editions);
|
||||
variantCards = new CardDb(variantsCards, editions);
|
||||
customCards = new CardDb(customizedCards, customEditions);
|
||||
commonCards = new CardDb(regularCards, editions, filtered);
|
||||
variantCards = new CardDb(variantsCards, editions, filtered);
|
||||
customCards = new CardDb(customizedCards, customEditions, filtered);
|
||||
|
||||
//must initialize after establish field values for the sake of card image logic
|
||||
commonCards.initialize(false, false, enableUnknownCards, loadNonLegalCards);
|
||||
variantCards.initialize(false, false, enableUnknownCards, loadNonLegalCards);
|
||||
customCards.initialize(false, false, enableUnknownCards, loadNonLegalCards);
|
||||
commonCards.initialize(false, false, enableUnknownCards);
|
||||
variantCards.initialize(false, false, enableUnknownCards);
|
||||
customCards.initialize(false, false, enableUnknownCards);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -125,14 +141,6 @@ public class StaticData {
|
||||
}
|
||||
allTokens = new TokenDb(tokens, editions);
|
||||
}
|
||||
|
||||
{
|
||||
if (customCards.getAllCards().size() > 0) {
|
||||
Collection<PaperCard> paperCards = customCards.getAllCards();
|
||||
for(PaperCard p: paperCards)
|
||||
commonCards.addCard(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static StaticData instance() {
|
||||
@@ -143,6 +151,11 @@ public class StaticData {
|
||||
return this.editions;
|
||||
}
|
||||
|
||||
public final CardEdition.Collection getCustomEditions(){
|
||||
return this.customEditions;
|
||||
}
|
||||
|
||||
|
||||
private List<CardEdition> sortedEditions;
|
||||
public final List<CardEdition> getSortedEditions() {
|
||||
if (sortedEditions == null) {
|
||||
@@ -150,12 +163,24 @@ public class StaticData {
|
||||
for (CardEdition set : editions) {
|
||||
sortedEditions.add(set);
|
||||
}
|
||||
if (customEditions.size() > 0){
|
||||
for (CardEdition set : customEditions) {
|
||||
sortedEditions.add(set);
|
||||
}
|
||||
}
|
||||
Collections.sort(sortedEditions);
|
||||
Collections.reverse(sortedEditions); //put newer sets at the top
|
||||
}
|
||||
return sortedEditions;
|
||||
}
|
||||
|
||||
public CardEdition getCardEdition(String setCode){
|
||||
CardEdition edition = this.editions.get(setCode);
|
||||
if (edition == null) // try custom editions
|
||||
edition = this.customEditions.get(setCode);
|
||||
return edition;
|
||||
}
|
||||
|
||||
public PaperCard getOrLoadCommonCard(String cardName, String setCode, int artIndex, boolean foil) {
|
||||
PaperCard card = commonCards.getCard(cardName, setCode, artIndex);
|
||||
boolean isCustom = false;
|
||||
|
||||
@@ -6,7 +6,6 @@ package forge.card;
|
||||
*/
|
||||
public class CardAiHints {
|
||||
|
||||
|
||||
private final boolean isRemovedFromAIDecks;
|
||||
private final boolean isRemovedFromRandomDecks;
|
||||
private final boolean isRemovedFromNonCommanderDecks;
|
||||
@@ -15,7 +14,6 @@ public class CardAiHints {
|
||||
private final DeckHints deckNeeds;
|
||||
private final DeckHints deckHas;
|
||||
|
||||
|
||||
public CardAiHints(boolean remAi, boolean remRandom, boolean remUnlessCommander, DeckHints dh, DeckHints dn, DeckHints has) {
|
||||
isRemovedFromAIDecks = remAi;
|
||||
isRemovedFromRandomDecks = remRandom;
|
||||
@@ -90,5 +88,4 @@ public class CardAiHints {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import forge.StaticData;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.commons.lang3.tuple.Pair;
|
||||
|
||||
@@ -55,6 +56,8 @@ import forge.util.TextUtil;
|
||||
public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
public final static String foilSuffix = "+";
|
||||
public final static char NameSetSeparator = '|';
|
||||
private final String exlcudedCardName = "Concentrate";
|
||||
private final String exlcudedCardSet = "DS0";
|
||||
|
||||
// need this to obtain cardReference by name+set+artindex
|
||||
private final ListMultimap<String, PaperCard> allCardsByName = Multimaps.newListMultimap(new TreeMap<>(String.CASE_INSENSITIVE_ORDER), CollectionSuppliers.arrayLists());
|
||||
@@ -66,8 +69,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
private final Map<String, String> alternateName = Maps.newTreeMap(String.CASE_INSENSITIVE_ORDER);
|
||||
private final Map<String, Integer> artIds = new HashMap<>();
|
||||
|
||||
private final Collection<PaperCard> roAllCards = Collections.unmodifiableCollection(allCardsByName.values());
|
||||
private final CardEdition.Collection editions;
|
||||
private List<String> filtered;
|
||||
|
||||
public enum SetPreference {
|
||||
Latest(false),
|
||||
@@ -134,12 +137,15 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
}
|
||||
|
||||
public CardDb(Map<String, CardRules> rules, CardEdition.Collection editions0) {
|
||||
public CardDb(Map<String, CardRules> rules, CardEdition.Collection editions0, List<String> filteredCards) {
|
||||
this.filtered = filteredCards;
|
||||
this.rulesByName = rules;
|
||||
this.editions = editions0;
|
||||
|
||||
// create faces list from rules
|
||||
for (final CardRules rule : rules.values() ) {
|
||||
if (filteredCards.contains(rule.getName()) && !exlcudedCardName.equalsIgnoreCase(rule.getName()))
|
||||
continue;
|
||||
final ICardFace main = rule.getMainPart();
|
||||
facesByName.put(main.getName(), main);
|
||||
if (main.getAltName() != null) {
|
||||
@@ -155,6 +161,10 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
}
|
||||
|
||||
private ListMultimap<String, PaperCard> getAllCardsByName() {
|
||||
return allCardsByName;
|
||||
}
|
||||
|
||||
private void addSetCard(CardEdition e, CardInSet cis, CardRules cr) {
|
||||
int artIdx = 1;
|
||||
String key = e.getCode() + "/" + cis.name;
|
||||
@@ -182,33 +192,24 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
reIndex();
|
||||
}
|
||||
|
||||
public void initialize(boolean logMissingPerEdition, boolean logMissingSummary, boolean enableUnknownCards, boolean loadNonLegalCards) {
|
||||
public void initialize(boolean logMissingPerEdition, boolean logMissingSummary, boolean enableUnknownCards) {
|
||||
Set<String> allMissingCards = new LinkedHashSet<>();
|
||||
List<String> missingCards = new ArrayList<>();
|
||||
CardEdition upcomingSet = null;
|
||||
Date today = new Date();
|
||||
List<String> skippedCardName = new ArrayList<>();
|
||||
|
||||
for (CardEdition e : editions.getOrderedEditions()) {
|
||||
boolean coreOrExpSet = e.getType() == CardEdition.Type.CORE || e.getType() == CardEdition.Type.EXPANSION;
|
||||
boolean isCoreExpSet = coreOrExpSet || e.getType() == CardEdition.Type.REPRINT;
|
||||
//todo sets with nonlegal cards should have tags in them so we don't need to specify the code here
|
||||
boolean skip = !loadNonLegalCards && (e.getType() == CardEdition.Type.FUNNY || e.getBorderColor() == CardEdition.BorderColor.SILVER);
|
||||
if (logMissingPerEdition && isCoreExpSet) {
|
||||
System.out.print(e.getName() + " (" + e.getAllCardsInSet().size() + " cards)");
|
||||
}
|
||||
if (coreOrExpSet && e.getDate().after(today) && upcomingSet == null) {
|
||||
if (skip)
|
||||
upcomingSet = e;
|
||||
upcomingSet = e;
|
||||
}
|
||||
|
||||
for (CardEdition.CardInSet cis : e.getAllCardsInSet()) {
|
||||
CardRules cr = rulesByName.get(cis.name);
|
||||
if (cr != null && !cr.getType().isBasicLand() && skip) {
|
||||
skippedCardName.add(cis.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cr != null) {
|
||||
addSetCard(e, cis, cr);
|
||||
}
|
||||
@@ -244,7 +245,7 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
if (!contains(cr.getName())) {
|
||||
if (upcomingSet != null) {
|
||||
addCard(new PaperCard(cr, upcomingSet.getCode(), CardRarity.Unknown, 1));
|
||||
} else if(enableUnknownCards && !skippedCardName.contains(cr.getName())) {
|
||||
} else if(enableUnknownCards) {
|
||||
System.err.println("The card " + cr.getName() + " was not assigned to any set. Adding it to UNKNOWN set... to fix see res/editions/ folder. ");
|
||||
addCard(new PaperCard(cr, CardEdition.UNKNOWN.getCode(), CardRarity.Special, 1));
|
||||
}
|
||||
@@ -255,6 +256,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
public void addCard(PaperCard paperCard) {
|
||||
if (excludeCard(paperCard.getName(), paperCard.getEdition()))
|
||||
return;
|
||||
|
||||
allCardsByName.put(paperCard.getName(), paperCard);
|
||||
|
||||
if (paperCard.getRules().getSplitType() == CardSplitType.None) { return; }
|
||||
@@ -268,11 +272,22 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
allCardsByName.put(paperCard.getRules().getMainPart().getName(), paperCard);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean excludeCard(String cardName, String cardEdition) {
|
||||
if (filtered.isEmpty())
|
||||
return false;
|
||||
if (filtered.contains(cardName)) {
|
||||
if (exlcudedCardSet.equalsIgnoreCase(cardEdition) && exlcudedCardName.equalsIgnoreCase(cardName))
|
||||
return true;
|
||||
else if (!exlcudedCardName.equalsIgnoreCase(cardName))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
private void reIndex() {
|
||||
uniqueCardsByName.clear();
|
||||
for (Entry<String, Collection<PaperCard>> kv : allCardsByName.asMap().entrySet()) {
|
||||
uniqueCardsByName.put(kv.getKey(), getFirstWithImage(kv.getValue()));
|
||||
for (Entry<String, Collection<PaperCard>> kv : getAllCardsByName().asMap().entrySet()) {
|
||||
PaperCard pc = getFirstWithImage(kv.getValue());
|
||||
uniqueCardsByName.put(kv.getKey(), pc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,6 +533,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
@Override
|
||||
public int getPrintCount(String cardName, String edition) {
|
||||
int cnt = 0;
|
||||
if (edition == null || cardName == null)
|
||||
return cnt;
|
||||
for (PaperCard pc : getAllCards(cardName)) {
|
||||
if (pc.getEdition().equals(edition)) {
|
||||
cnt++;
|
||||
@@ -529,6 +546,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
@Override
|
||||
public int getMaxPrintCount(String cardName) {
|
||||
int max = -1;
|
||||
if (cardName == null)
|
||||
return max;
|
||||
for (PaperCard pc : getAllCards(cardName)) {
|
||||
if (max < pc.getArtIndex()) {
|
||||
max = pc.getArtIndex();
|
||||
@@ -540,6 +559,8 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
@Override
|
||||
public int getArtCount(String cardName, String setName) {
|
||||
int cnt = 0;
|
||||
if (cardName == null || setName == null)
|
||||
return cnt;
|
||||
|
||||
Collection<PaperCard> cards = getAllCards(cardName);
|
||||
if (null == cards) {
|
||||
@@ -561,6 +582,17 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
return uniqueCardsByName.values();
|
||||
}
|
||||
|
||||
public Collection<PaperCard> getUniqueCardsNoAlt() {
|
||||
return Maps.filterEntries(this.uniqueCardsByName, new Predicate<Entry<String, PaperCard>>() {
|
||||
@Override
|
||||
public boolean apply(Entry<String, PaperCard> e) {
|
||||
if (e == null)
|
||||
return false;
|
||||
return e.getKey().equals(e.getValue().getName());
|
||||
}
|
||||
}).values();
|
||||
}
|
||||
|
||||
public PaperCard getUniqueByName(final String name) {
|
||||
return uniqueCardsByName.get(getName(name));
|
||||
}
|
||||
@@ -575,11 +607,20 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
|
||||
@Override
|
||||
public Collection<PaperCard> getAllCards() {
|
||||
return roAllCards;
|
||||
return Collections.unmodifiableCollection(getAllCardsByName().values());
|
||||
}
|
||||
|
||||
public Collection<PaperCard> getAllCardsNoAlt() {
|
||||
return Multimaps.filterEntries(getAllCardsByName(), new Predicate<Entry<String, PaperCard>>() {
|
||||
@Override
|
||||
public boolean apply(Entry<String, PaperCard> entry) {
|
||||
return entry.getKey().equals(entry.getValue().getName());
|
||||
}
|
||||
}).values();
|
||||
}
|
||||
|
||||
public Collection<PaperCard> getAllNonPromoCards() {
|
||||
return Lists.newArrayList(Iterables.filter(this.roAllCards, new Predicate<PaperCard>() {
|
||||
return Lists.newArrayList(Iterables.filter(getAllCards(), new Predicate<PaperCard>() {
|
||||
@Override
|
||||
public boolean apply(final PaperCard paperCard) {
|
||||
CardEdition edition = null;
|
||||
@@ -593,6 +634,23 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}));
|
||||
}
|
||||
|
||||
public Collection<PaperCard> getAllNonPromosNonReprintsNoAlt() {
|
||||
return Lists.newArrayList(Iterables.filter(getAllCardsNoAlt(), new Predicate<PaperCard>() {
|
||||
@Override
|
||||
public boolean apply(final PaperCard paperCard) {
|
||||
CardEdition edition = null;
|
||||
try {
|
||||
edition = editions.getEditionByCodeOrThrow(paperCard.getEdition());
|
||||
if (edition.getType() == Type.PROMOS||edition.getType() == Type.REPRINT)
|
||||
return false;
|
||||
} catch (Exception ex) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
public String getName(final String cardName) {
|
||||
if (alternateName.containsKey(cardName)) {
|
||||
return alternateName.get(cardName);
|
||||
@@ -602,13 +660,27 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
|
||||
@Override
|
||||
public List<PaperCard> getAllCards(String cardName) {
|
||||
return allCardsByName.get(getName(cardName));
|
||||
return getAllCardsByName().get(getName(cardName));
|
||||
}
|
||||
|
||||
public List<PaperCard> getAllCardsNoAlt(String cardName) {
|
||||
return Lists.newArrayList(Multimaps.filterEntries(getAllCardsByName(), new Predicate<Entry<String, PaperCard>>() {
|
||||
@Override
|
||||
public boolean apply(Entry<String, PaperCard> entry) {
|
||||
return entry.getKey().equals(entry.getValue().getName());
|
||||
}
|
||||
}).get(getName(cardName)));
|
||||
}
|
||||
|
||||
/** Returns a modifiable list of cards matching the given predicate */
|
||||
@Override
|
||||
public List<PaperCard> getAllCards(Predicate<PaperCard> predicate) {
|
||||
return Lists.newArrayList(Iterables.filter(this.roAllCards, predicate));
|
||||
return Lists.newArrayList(Iterables.filter(getAllCards(), predicate));
|
||||
}
|
||||
|
||||
/** Returns a modifiable list of cards matching the given predicate */
|
||||
public List<PaperCard> getAllCardsNoAlt(Predicate<PaperCard> predicate) {
|
||||
return Lists.newArrayList(Iterables.filter(getAllCardsNoAlt(), predicate));
|
||||
}
|
||||
|
||||
// Do I want a foiled version of these cards?
|
||||
@@ -630,12 +702,12 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
|
||||
@Override
|
||||
public boolean contains(String name) {
|
||||
return allCardsByName.containsKey(getName(name));
|
||||
return getAllCardsByName().containsKey(getName(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<PaperCard> iterator() {
|
||||
return this.roAllCards.iterator();
|
||||
return getAllCards().iterator();
|
||||
}
|
||||
|
||||
public Predicate<? super PaperCard> wasPrintedInSets(List<String> setCodes) {
|
||||
@@ -700,7 +772,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
public PaperCard createUnsupportedCard(String cardName) {
|
||||
|
||||
CardRequest request = CardRequest.fromString(cardName);
|
||||
CardEdition cardEdition = CardEdition.UNKNOWN;
|
||||
CardRarity cardRarity = CardRarity.Unknown;
|
||||
@@ -741,7 +812,6 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
|
||||
}
|
||||
|
||||
return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1);
|
||||
|
||||
}
|
||||
|
||||
private final Editor editor = new Editor();
|
||||
|
||||
@@ -19,7 +19,6 @@ package forge.card;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FilenameFilter;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
@@ -71,7 +70,9 @@ import forge.util.storage.StorageReaderFolder;
|
||||
* @author Forge
|
||||
* @version $Id: CardSet.java 9708 2011-08-09 19:34:12Z jendave $
|
||||
*/
|
||||
public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
public final class CardEdition implements Comparable<CardEdition> {
|
||||
|
||||
// immutable
|
||||
public enum Type {
|
||||
UNKNOWN,
|
||||
|
||||
@@ -116,18 +117,22 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
}
|
||||
|
||||
// reserved names of sections inside edition files, that are not parsed as cards
|
||||
private static final List<String> reservedSectionNames = ImmutableList.of("metadata", "tokens");
|
||||
private static final List<String> reservedSectionNames = ImmutableList.of("metadata", "tokens", "other");
|
||||
|
||||
// commonly used printsheets with collector number
|
||||
public enum EditionSectionWithCollectorNumbers {
|
||||
CARDS("cards"),
|
||||
SPECIAL_SLOT("special slot"), //to help with convoluted boosters
|
||||
PRECON_PRODUCT("precon product"),
|
||||
BORDERLESS("borderless"),
|
||||
SHOWCASE("showcase"),
|
||||
EXTENDED_ART("extended art"),
|
||||
ALTERNATE_ART("alternate art"),
|
||||
ALTERNATE_FRAME("alternate frame"),
|
||||
BUY_A_BOX("buy a box"),
|
||||
PROMO("promo");
|
||||
PROMO("promo"),
|
||||
BUNDLE("bundle"),
|
||||
BOX_TOPPER("box topper");
|
||||
|
||||
private final String name;
|
||||
|
||||
@@ -147,7 +152,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
}
|
||||
}
|
||||
|
||||
public static class CardInSet {
|
||||
public static class CardInSet implements Comparable<CardInSet> {
|
||||
public final CardRarity rarity;
|
||||
public final String collectorNumber;
|
||||
public final String name;
|
||||
@@ -171,6 +176,56 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
sb.append(name);
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method implements the main strategy to allow for natural ordering of collectorNumber
|
||||
* (i.e. "1" < "10"), overloading the default lexicographic order (i.e. "10" < "1").
|
||||
* Any non-numerical parts in the input collectorNumber will be also accounted for, and attached to the
|
||||
* resulting sorting key, accordingly.
|
||||
*
|
||||
* @param collectorNumber: Input collectorNumber tro transform in a Sorting Key
|
||||
* @return A 5-digits zero-padded collector number + any non-numerical parts attached.
|
||||
*/
|
||||
public static String getSortableCollectorNumber(final String collectorNumber){
|
||||
String sortableCollNr = collectorNumber;
|
||||
if (sortableCollNr == null || sortableCollNr.length() == 0)
|
||||
sortableCollNr = "50000"; // very big number of 5 digits to have them in last positions
|
||||
|
||||
// Now, for proper sorting, let's zero-pad the collector number (if integer)
|
||||
int collNr;
|
||||
try {
|
||||
collNr = Integer.parseInt(sortableCollNr);
|
||||
sortableCollNr = String.format("%05d", collNr);
|
||||
} catch (NumberFormatException ex) {
|
||||
String nonNumeric = sortableCollNr.replaceAll("[0-9]", "");
|
||||
String onlyNumeric = sortableCollNr.replaceAll("[^0-9]", "");
|
||||
try {
|
||||
collNr = Integer.parseInt(onlyNumeric);
|
||||
} catch (NumberFormatException exon) {
|
||||
collNr = 0; // this is the case of ONLY-letters collector numbers
|
||||
}
|
||||
if ((collNr > 0) && (sortableCollNr.startsWith(onlyNumeric))) // e.g. 12a, 37+, 2018f,
|
||||
sortableCollNr = String.format("%05d", collNr) + nonNumeric;
|
||||
else // e.g. WS6, S1
|
||||
sortableCollNr = nonNumeric + String.format("%05d", collNr);
|
||||
}
|
||||
return sortableCollNr;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CardInSet o) {
|
||||
final int nameCmp = name.compareToIgnoreCase(o.name);
|
||||
if (0 != nameCmp) {
|
||||
return nameCmp;
|
||||
}
|
||||
String thisCollNr = getSortableCollectorNumber(collectorNumber);
|
||||
String othrCollNr = getSortableCollectorNumber(o.collectorNumber);
|
||||
final int collNrCmp = thisCollNr.compareTo(othrCollNr);
|
||||
if (0 != collNrCmp) {
|
||||
return collNrCmp;
|
||||
}
|
||||
return rarity.compareTo(o.rarity);
|
||||
}
|
||||
}
|
||||
|
||||
private final static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
|
||||
@@ -181,6 +236,8 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
private String code;
|
||||
private String code2;
|
||||
private String mciCode;
|
||||
private String scryfallCode;
|
||||
private String cardsLanguage;
|
||||
private Type type;
|
||||
private String name;
|
||||
private String alias = null;
|
||||
@@ -205,6 +262,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
private String[] chaosDraftThemes = new String[0];
|
||||
|
||||
private final ListMultimap<String, CardInSet> cardMap;
|
||||
private final List<CardInSet> cardsInSet;
|
||||
private final Map<String, Integer> tokenNormalized;
|
||||
// custom print sheets that will be loaded lazily
|
||||
private final Map<String, List<String>> customPrintSheetsToParse;
|
||||
@@ -214,13 +272,18 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
|
||||
private CardEdition(ListMultimap<String, CardInSet> cardMap, Map<String, Integer> tokens, Map<String, List<String>> customPrintSheetsToParse) {
|
||||
this.cardMap = cardMap;
|
||||
this.cardsInSet = new ArrayList<>(cardMap.values());
|
||||
Collections.sort(cardsInSet);
|
||||
this.tokenNormalized = tokens;
|
||||
this.customPrintSheetsToParse = customPrintSheetsToParse;
|
||||
}
|
||||
|
||||
private CardEdition(CardInSet[] cards, Map<String, Integer> tokens) {
|
||||
List<CardInSet> cardsList = Arrays.asList(cards);
|
||||
this.cardMap = ArrayListMultimap.create();
|
||||
this.cardMap.replaceValues("cards", Arrays.asList(cards));
|
||||
this.cardMap.replaceValues("cards", cardsList);
|
||||
this.cardsInSet = new ArrayList<>(cardsList);
|
||||
Collections.sort(cardsInSet);
|
||||
this.tokenNormalized = tokens;
|
||||
this.customPrintSheetsToParse = new HashMap<>();
|
||||
}
|
||||
@@ -255,7 +318,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
date = date + "-01";
|
||||
try {
|
||||
return formatter.parse(date);
|
||||
} catch (ParseException e) {
|
||||
} catch (Exception e) {
|
||||
return new Date();
|
||||
}
|
||||
}
|
||||
@@ -264,6 +327,8 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
public String getCode() { return code; }
|
||||
public String getCode2() { return code2; }
|
||||
public String getMciCode() { return mciCode; }
|
||||
public String getScryfallCode() { return scryfallCode.toLowerCase(); }
|
||||
public String getCardsLangCode() { return cardsLanguage.toLowerCase(); }
|
||||
public Type getType() { return type; }
|
||||
public String getName() { return name; }
|
||||
public String getAlias() { return alias; }
|
||||
@@ -286,7 +351,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
|
||||
public List<CardInSet> getCards() { return cardMap.get("cards"); }
|
||||
public List<CardInSet> getAllCardsInSet() {
|
||||
return Lists.newArrayList(cardMap.values());
|
||||
return cardsInSet;
|
||||
}
|
||||
|
||||
public boolean isModern() { return getDate().after(parseDate("2003-07-27")); } //8ED and above are modern except some promo cards and others
|
||||
@@ -305,7 +370,10 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
if (o == null) {
|
||||
return 1;
|
||||
}
|
||||
return date.compareTo(o.date);
|
||||
int dateComp = date.compareTo(o.date);
|
||||
if (0 != dateComp)
|
||||
return dateComp;
|
||||
return name.compareTo(o.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -339,7 +407,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
}
|
||||
|
||||
public boolean isLargeSet() {
|
||||
return getAllCardsInSet().size() > 200 && !smallSetOverride;
|
||||
return this.cardsInSet.size() > 200 && !smallSetOverride;
|
||||
}
|
||||
|
||||
public int getCntBoosterPictures() {
|
||||
@@ -413,7 +481,8 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
* rarity - grouping #4
|
||||
* name - grouping #5
|
||||
*/
|
||||
"(^([0-9]+.?) )?(([SCURML]) )?(.*)$"
|
||||
// "(^(.?[0-9A-Z]+.?))?(([SCURML]) )?(.*)$"
|
||||
"(^(.?[0-9A-Z]+\\S?[A-Z]*)\\s)?(([SCURML])\\s)?(.*)$"
|
||||
);
|
||||
|
||||
ListMultimap<String, CardInSet> cardMap = ArrayListMultimap.create();
|
||||
@@ -479,6 +548,14 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
|
||||
if (res.mciCode == null) {
|
||||
res.mciCode = res.code2.toLowerCase();
|
||||
}
|
||||
res.scryfallCode = section.get("ScryfallCode");
|
||||
if (res.scryfallCode == null){
|
||||
res.scryfallCode = res.code;
|
||||
}
|
||||
res.cardsLanguage = section.get("CardLang");
|
||||
if (res.cardsLanguage == null){
|
||||
res.cardsLanguage = "en";
|
||||
}
|
||||
|
||||
res.boosterArts = section.getInt("BoosterCovers", 1);
|
||||
String boosterDesc = section.get("Booster");
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
*/
|
||||
package forge.card;
|
||||
|
||||
import java.util.StringTokenizer;
|
||||
import java.util.*;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
@@ -28,6 +28,9 @@ import forge.card.mana.ManaCost;
|
||||
import forge.card.mana.ManaCostShard;
|
||||
import forge.util.TextUtil;
|
||||
|
||||
import static forge.card.MagicColor.Constant.*;
|
||||
import static org.apache.commons.lang3.StringUtils.containsIgnoreCase;
|
||||
|
||||
/**
|
||||
* A collection of methods containing full
|
||||
* meta and gameplay properties of a card.
|
||||
@@ -42,6 +45,7 @@ public final class CardRules implements ICardCharacteristics {
|
||||
private ICardFace otherPart;
|
||||
private CardAiHints aiHints;
|
||||
private ColorSet colorIdentity;
|
||||
private ColorSet deckbuildingColors;
|
||||
private String meldWith;
|
||||
private String partnerWith;
|
||||
|
||||
@@ -80,6 +84,12 @@ public final class CardRules implements ICardCharacteristics {
|
||||
boolean isReminder = false;
|
||||
boolean isSymbol = false;
|
||||
String oracleText = face.getOracleText();
|
||||
// CR 903.4 colors defined by its characteristic-defining abilities
|
||||
for (String staticAbility : face.getStaticAbilities()) {
|
||||
if (staticAbility.contains("CharacteristicDefining$ True") && staticAbility.contains("SetColor$ All")) {
|
||||
res |= MagicColor.ALL_COLORS;
|
||||
}
|
||||
}
|
||||
int len = oracleText.length();
|
||||
for(int i = 0; i < len; i++) {
|
||||
char c = oracleText.charAt(i); // This is to avoid needless allocations performed by toCharArray()
|
||||
@@ -384,7 +394,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
this.removedFromNonCommanderDecks = "NonCommander".equalsIgnoreCase(value);
|
||||
}
|
||||
} else if ("AlternateMode".equals(key)) {
|
||||
//System.out.println(faces[curFace].getName());
|
||||
this.altMode = CardSplitType.smartValueOf(value);
|
||||
} else if ("ALTERNATE".equals(key)) {
|
||||
this.curFace = 1;
|
||||
@@ -535,7 +544,6 @@ public final class CardRules implements ICardCharacteristics {
|
||||
@Override
|
||||
public final ManaCostShard next() {
|
||||
final String unparsed = st.nextToken();
|
||||
// System.out.println(unparsed);
|
||||
if (StringUtils.isNumeric(unparsed)) {
|
||||
this.genericCost += Integer.parseInt(unparsed);
|
||||
return null;
|
||||
@@ -551,7 +559,7 @@ public final class CardRules implements ICardCharacteristics {
|
||||
*/
|
||||
@Override
|
||||
public void remove() {
|
||||
} // unsuported
|
||||
} // unsupported
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,4 +589,25 @@ public final class CardRules implements ICardCharacteristics {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ColorSet getDeckbuildingColors() {
|
||||
if (deckbuildingColors == null) {
|
||||
byte colors = 0;
|
||||
if (mainPart.getType().isLand()) {
|
||||
colors = getColorIdentity().getColor();
|
||||
for (int i = 0; i < 5; i++) {
|
||||
if (containsIgnoreCase(mainPart.getOracleText(), BASIC_LANDS.get(i))) {
|
||||
colors |= 1 << i;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
colors = getColor().getColor();
|
||||
if (getOtherPart() != null) {
|
||||
colors |= getOtherPart().getManaCost().getColorProfile();
|
||||
}
|
||||
}
|
||||
deckbuildingColors = ColorSet.fromMask(colors);
|
||||
}
|
||||
return deckbuildingColors;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +91,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
|
||||
public enum Supertype {
|
||||
Basic,
|
||||
Elite,
|
||||
Host,
|
||||
Legendary,
|
||||
Snow,
|
||||
Ongoing,
|
||||
|
||||
@@ -138,23 +138,11 @@ public final class MagicColor {
|
||||
public static final ImmutableList<String> BASIC_LANDS = ImmutableList.of("Plains", "Island", "Swamp", "Mountain", "Forest");
|
||||
public static final ImmutableList<String> SNOW_LANDS = ImmutableList.of("Snow-Covered Plains", "Snow-Covered Island", "Snow-Covered Swamp", "Snow-Covered Mountain", "Snow-Covered Forest");
|
||||
public static final ImmutableMap<String, String> ANY_COLOR_CONVERSION = new ImmutableMap.Builder<String, String>()
|
||||
.put("ManaColorConversion", "Additive")
|
||||
.put("WhiteConversion", "Color")
|
||||
.put("BlueConversion", "Color")
|
||||
.put("BlackConversion", "Color")
|
||||
.put("RedConversion", "Color")
|
||||
.put("GreenConversion", "Color")
|
||||
.put("ColorlessConversion", "Color")
|
||||
.put("ManaConversion", "AnyType->AnyColor")
|
||||
.build();
|
||||
|
||||
public static final ImmutableMap<String, String> ANY_TYPE_CONVERSION = new ImmutableMap.Builder<String, String>()
|
||||
.put("ManaColorConversion", "Additive")
|
||||
.put("WhiteConversion", "Type")
|
||||
.put("BlueConversion", "Type")
|
||||
.put("BlackConversion", "Type")
|
||||
.put("RedConversion", "Type")
|
||||
.put("GreenConversion", "Type")
|
||||
.put("ColorlessConversion", "Type")
|
||||
.put("ManaConversion", "AnyType->AnyType")
|
||||
.build();
|
||||
/**
|
||||
* Private constructor to prevent instantiation.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user