Compare commits

...

1719 Commits

Author SHA1 Message Date
Hans Mackowiak
1efddc9a92 CardTraitChanges: as Record 2025-08-30 10:44:01 +02:00
kevlahnota
01d5bf3c21 Merge pull request #8521 from antoniomartinelli/fix/lastimportedcubeid
fix: wrong cube id saved as LAST_IMPORTED_CUBE_ID
2025-08-30 06:55:02 +08:00
kevlahnota
29a630386f Merge pull request #8410 from rappazzo/stop-tracking-changelog
fix: move CHANGES.txt generation to target directory
2025-08-30 06:31:10 +08:00
Hans Mackowiak
75c7938f1e CounterType: turn wrapper into an interface (#8572)
* CounterType: turn wrapper into an interface

* remove CounterType.get(CounterEnumType) helper
2025-08-29 20:44:08 +02:00
Fulgur14
110e885c67 Create with_great_power.txt (#8573) 2025-08-29 18:40:39 +00:00
Hans Mackowiak
548b448f0d Update Game.java
`getNonactivePlayers` not needed anymore
2025-08-29 20:38:55 +02:00
Hans Mackowiak
a2e80ac0f3 TriggerHandler: no need for APNAP (#8567)
* TriggerHandler: no need for APNAP

The order of the Trigger will be handled later in MagicStack

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-29 20:37:26 +02:00
Eradev
238d426202 Alchemy update 20250818 (#8492) 2025-08-29 17:55:18 +00:00
Hans Mackowiak
d6f585a80c Event: use Record 2025-08-29 18:05:05 +02:00
tool4ever
223aeb1bff Update displaced_dinosaurs.txt 2025-08-29 12:02:27 +02:00
Hans Mackowiak
4bf02ea61f MagicStack: remove getNonactivePlayers for SpellCastSinceBegOfYourLastTurn 2025-08-29 11:20:38 +02:00
tool4ever
ea186ae4af Update faunsbane_troll.txt 2025-08-28 19:26:09 +00:00
kevlahnota
3e4cd9e189 Update LoadDraftScreen.java
prevent IllegalStateException since the option pane show method needs to run in EDT
2025-08-28 20:13:01 +08:00
Eradev
e35c193f92 Fix content passed 2025-08-27 22:36:22 -04:00
Eradev
e5443fc394 Typo 2025-08-27 22:36:22 -04:00
Eradev
9998092c70 Create helper class to not duplicate the code 2025-08-27 22:36:22 -04:00
Eradev
9f81e0cd34 Move custom types into edition files 2025-08-27 22:36:22 -04:00
Eradev
5569f18053 Remove param 2025-08-27 22:36:22 -04:00
Eradev
40190b5442 Remove TreeSet 2025-08-27 22:36:22 -04:00
Eradev
a33905241d Revert "Ensure subtype not used"
This reverts commit 6797f0c1b0.
2025-08-27 22:36:22 -04:00
Eradev
3a40b560f6 Removed import 2025-08-27 22:36:22 -04:00
Eradev
d79fb96770 Ensure subtype not used 2025-08-27 22:36:22 -04:00
Eradev
f50fe2f89c Now read from a directory 2025-08-27 22:36:22 -04:00
Eradev
a36deab334 Allow custom types 2025-08-27 22:36:22 -04:00
kevlahnota
28feff99d7 Merge pull request #8565 from kevlahnota/master4
prevent NPE, ConcurrentModification on ImageView onRefresh
2025-08-28 10:04:57 +08:00
Anthony Calosa
bab8791012 prevent NPE, ConcurrentModification on ImageView onRefresh
use Record for ImageRecord
2025-08-28 06:28:34 +08:00
Fulgur14
db20b27c9d Update soul_seizer_ghastly_haunting.txt (#8431) 2025-08-27 15:20:24 +00:00
Renato Filipe Vidal Santos
087db3f757 YEOE: 5 cards (#8484) 2025-08-27 15:16:24 +00:00
kevlahnota
db194fab97 Merge pull request #8561 from kevlahnota/master4
Remove unsupported cards from AdventurePlayer inventory
2025-08-27 21:52:52 +08:00
Anthony Calosa
0585ece2c1 Remove unsupported cards from AdventurePlayer inventory
- closes #8545
2025-08-27 21:34:07 +08:00
Eradev
1611559909 Add new card filters (#8557)
* Add is:vanilla

* Add is:custom
2025-08-27 06:10:49 +00:00
Cees Timmerman
d6dee9575b More human-readable events (#8502) 2025-08-27 07:23:14 +02:00
Cees Timmerman
6dd9a731fa Fix event.oldEntiy typo 2025-08-27 07:23:14 +02:00
Leandro Doctors
cf5a8508d6 Remove empty test class
Dead, unmaintained code eventually leads to problems.
2025-08-27 06:56:12 +02:00
tool4ever
0fa7df090e Fix Muraganda Petroglyphs vs. Saga with no abilities (#8551)
---------

Co-authored-by: TRT <>
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-27 06:51:15 +02:00
kevlahnota
86838d94f7 Merge pull request #8544 from Eradev/RemoveUnsupportedCard
Option to remove unsupported card from collection
2025-08-27 12:21:53 +08:00
kevlahnota
83c4db07ac Merge pull request #8559 from Eradev/RemoveAdvCardsRewards
Remove custom adventure cards from rewards.
2025-08-27 12:19:58 +08:00
Eradev
b544f2dd00 Remove custom adventure cards from rewards. 2025-08-26 20:54:26 -04:00
kevlahnota
079e6f04ac update canBeOathbreaker check (#8556) 2025-08-27 05:22:00 +08:00
Eradev
d04c541578 Fix edition mapping 2025-08-26 10:34:41 -04:00
Hans Mackowiak
7cc2cf530d AnimateBase: PerpetualManaCost 2025-08-26 15:57:50 +02:00
Hans Mackowiak
61a2c7cadb Update DamageAllAi.java (#8550)
Remove `getNonactivePlayers`
2025-08-26 15:51:39 +02:00
tool4ever
efbf2e1a9c Saga ETB counters aren't intrinsic (#8549) 2025-08-26 15:47:17 +02:00
Anthony Calosa
9b8441d45b update Console Textfield navigation 2025-08-26 21:04:06 +08:00
Hans Mackowiak
f8b7a0fb9a MagicStack: check for StackEntries in addAllTriggeredAbilities 2025-08-26 14:56:24 +02:00
shenshinoman
ab49e97797 Expanding my Innistrad further. new enemies. New maps. Second biome is partially populated. Everything is contained to innistrad plane, so should have no impact on the main game outside that plane. 2025-08-26 08:40:30 -04:00
shenshinoman
c27a9d136f Expanding my Innistrad further. new enemies. New maps. Second biome is partially populated. Everything is contained to innistrad plane, so should have no impact on the main game outside that plane. 2025-08-26 08:40:30 -04:00
Leandro Doctors
91241ee53c clean up: remove obsolete GitLab templates
The project moved to GitHub almost a year ago.

Having duplicated, unmaintained templates will generate problems in the
future. If needed, they can always be retrieved from the commit history.
2025-08-26 13:32:30 +02:00
Eradev
31e537f42b Remove changes to FContainer 2025-08-26 06:33:30 -04:00
Eradev
e550c307c2 Option to remove unsupported card from collection 2025-08-26 06:29:49 -04:00
Paul Hammerton
b4f01b7ebb Merge pull request #8542 from paulsnoops/treetop-recluse
Add treetop_recluse.txt
2025-08-26 09:10:59 +01:00
Eradev
7744474f39 Fix token numbers 2025-08-26 09:58:01 +02:00
Paul Hammerton
1365b82968 Add treetop_recluse.txt 2025-08-26 08:43:01 +01:00
Hans Mackowiak
5bb532bf9d Game: only reverse NonactivePlayers if more than one
Reversing the Turn Order should have no Effect there if only 2 players
2025-08-26 07:35:39 +02:00
tool4ever
258b8c18a9 Fix Thief of Blood simultaneous ETB (#8535)
Co-authored-by: TRT <>
2025-08-26 08:26:32 +03:00
Eradev
a66349d8a1 Missing token (#8538)
* Add g_1_1_forest_dryad_squirrel

* Update curiosity
2025-08-26 08:26:25 +03:00
kevlahnota
1933cf1863 Merge pull request #8537 from kevlahnota/master3
migrate Callback to Consumer Interface
2025-08-26 12:59:53 +08:00
kevlahnota
0d342c778f fix NG+ 2025-08-26 09:11:31 +08:00
kevlahnota
f452b94cb8 update missing migration 2025-08-26 07:16:43 +08:00
kevlahnota
9325794e2f Update FFileChooser.java 2025-08-26 07:03:24 +08:00
kevlahnota
192a64bbc3 Update Forge.java 2025-08-26 06:58:09 +08:00
kevlahnota
deb8369f11 Update FDeckChooser.java 2025-08-26 06:53:59 +08:00
Anthony Calosa
b24f536190 use Java Consumer 2025-08-26 06:34:59 +08:00
Leandro Doctors
28ec24069c CI: add support for JDK 21
Depends on https://github.com/Card-Forge/forge/pull/8533
2025-08-25 17:24:19 -04:00
Anthony Calosa
f27472d9bd migrate Callback to Interface
- closes #5717
2025-08-26 05:14:20 +08:00
Leandro Doctors
780cc8ddbf Fix build on JDK 21+
This bumps `izpack-maven-plugin`.

Tested with JDK 25.

Similar fix for another project:
https://github.com/lsc-project/lsc/pull/385

Original Error message:
`[ERROR] Failed to execute goal
org.codehaus.izpack:izpack-maven-plugin:5.2.3:izpack
(standard-installer) on project forge-installer: Execution
standard-installer
of goal org.codehaus.izpack:izpack-maven-plugin:5.2.3:izpack failed:
java.lang.ArrayIndexOutOfBoundsException: Index 70131 out of bounds for
length 22674 -> [Help 1]`
2025-08-25 13:50:57 -04:00
kevlahnota
23555b9564 migrate function to method reference 2025-08-25 19:54:42 +08:00
kevlahnota
431827be35 Update Keyword.java for Double Team
- closes #8519
2025-08-25 11:49:49 +08:00
kevlahnota
b0dba74c6c Merge pull request #8528 from kevlahnota/master3
update ConsoleCommandInterpreter
2025-08-25 06:56:58 +08:00
Anthony Calosa
ad0a690764 minor typo 2025-08-25 06:56:27 +08:00
Anthony Calosa
977f2c75b9 update comment 2025-08-25 06:53:28 +08:00
Anthony Calosa
8313414f78 update ConsoleCommandInterpreter 2025-08-25 06:49:59 +08:00
kevlahnota
89955bf201 Merge pull request #8527 from kevlahnota/master3
capture adventure SaveFileData exception to sentry
2025-08-25 05:38:14 +08:00
Anthony Calosa
99d191901a capture adventure SaveFileData exception to sentry 2025-08-25 05:33:45 +08:00
kevlahnota
1a306b3da3 Merge pull request #8525 from kevlahnota/master3
prevent saving error on inventory
2025-08-25 04:55:51 +08:00
Anthony Calosa
9ff03cbd41 prevent saving error on inventory 2025-08-25 04:50:58 +08:00
tool4ever
07a1dbc099 Fix scripts (#8524) 2025-08-24 22:10:56 +02:00
Hans Mackowiak
f24b3ea3b3 Update enchantment_alteration.txt
closes #8522
2025-08-24 22:02:36 +02:00
kevlahnota
770cc72fcd Merge pull request #8523 from kevlahnota/master3
update migration message
2025-08-25 03:36:27 +08:00
Anthony Calosa
8426a74900 update migration message 2025-08-25 03:34:43 +08:00
antoniomartinelli
8d98eda18d fix: wrong cube id saved as LAST_IMPORTED_CUBE_ID 2025-08-24 18:17:54 +02:00
Paul Hammerton
6e6509eaff Merge pull request #8518 from paulsnoops/edition-updates
Edition updates: PSPM
2025-08-24 11:13:05 +01:00
Paul Hammerton
c56ddee47d Edition updates: PSPM 2025-08-24 11:11:41 +01:00
Eradev
07985ac487 Allow custom cards as reward (#8512) 2025-08-23 21:55:28 -04:00
kevlahnota
6c93491d7f Don't select sideboard first when creating new deck 2025-08-24 05:13:36 +08:00
Eradev
c9b012c88a Fix Bloomburrow lands (#8489)
* Fix Bloomburrow lands

* Proper fix

* Revert "Proper fix"

This reverts commit f3a46e40d9.

* Use collector number
2025-08-23 14:14:57 -04:00
kevlahnota
9b53144976 Merge pull request #8509 from kevlahnota/master3
remove Normalizer from clipboard and PaperCard artist.
2025-08-24 00:53:22 +08:00
Anthony Calosa
31020296d8 unused import 2025-08-24 00:46:02 +08:00
Anthony Calosa
65fb3414d8 remove Normalizer from clipboard and PaperCard artist. 2025-08-24 00:42:40 +08:00
kevlahnota
98fee0d86b Merge pull request #8497 from kevlahnota/master3
update InventoryScene, add repair cracked item for gold
2025-08-23 23:17:08 +08:00
Anthony Calosa
becdadb279 NPE prevention for rewards 2025-08-23 23:08:11 +08:00
Hans Mackowiak
26da0ab0d4 Create rw_1_1_soldier.txt
Fix missing TokenScript
2025-08-23 16:46:16 +02:00
Anthony Calosa
e1f4d755e0 minor fix for refreshing overlay 2025-08-23 21:38:52 +08:00
Anthony Calosa
efe7d67d9f add unusable indicator 2025-08-23 21:34:57 +08:00
Anthony Calosa
3a5e11504a add migration message, fix cloning of itemData 2025-08-23 20:07:28 +08:00
Fulgur14
6657602b80 J. Jonah Jameson (SPM) (#8506) 2025-08-23 11:00:45 +02:00
Eradev
eeca33855d Fix minor card render problems (#8478)
* Missing space

* Unescape line breaks in card descriptions
2025-08-23 06:56:21 +03:00
Matthew Scott Krafczyk
cdc63f35bb Add python specific entries to .gitignore (#8390) 2025-08-23 06:55:28 +03:00
Jetz72
5a9ea8d260 Merge pull request #8344 from verifiedtm/deck-editor-sections
Add other deck sections for other formats
2025-08-22 07:56:55 -05:00
Fulgur14
5a363922bd Oracle updates - EOE (#8496) 2025-08-22 12:57:01 +02:00
Anthony Calosa
1ff16ca509 prevent NPE 2025-08-22 16:43:17 +08:00
TRT
c878401197 Script cleanup 2025-08-22 10:03:27 +02:00
Eradev
8f518b7b1f Fix IsDoctorCheck 2025-08-22 06:33:29 +02:00
Anthony Calosa
fb624458f0 update InventoryScene, add repair cracked item for gold 2025-08-22 12:32:12 +08:00
Paul Hammerton
ee3220f33b Merge pull request #8487 from paulsnoops/fix-yeoe
YEOE edition and format updates for check land cycle
2025-08-21 19:31:16 +01:00
Paul Hammerton
7bc591fa78 YEOE mana-fixing lands edtion and format update 2025-08-21 19:25:47 +01:00
kevlahnota
c02d942fc3 Merge pull request #8485 from kevlahnota/master3
request access if needed to use backup and restore
2025-08-21 22:00:02 +08:00
Anthony Calosa
3b9ded8270 request access if needed to use backup and restore 2025-08-21 21:49:45 +08:00
tool4ever
fdf0c13a7f Remove outdated Briar Shield code path (#8477) 2025-08-21 07:15:21 +01:00
Hans Mackowiak
80b1cac394 lf 2025-08-21 07:05:35 +02:00
Hans Mackowiak
8c80c32113 lf 2025-08-21 07:01:46 +02:00
Agetian
a74b033c26 Revert "Ambrosia Whiteheart AI slight improvement (#8473)" (#8483)
This reverts commit ba8a30ebdd.
2025-08-21 07:48:20 +03:00
kevlahnota
16fcbc0ebf Merge pull request #8481 from kevlahnota/master3
fix osFamily name for sentry, update android-all
2025-08-21 09:54:47 +08:00
Anthony Calosa
62ffada6fe update cpu name 2025-08-21 09:46:30 +08:00
Anthony Calosa
e5a0d335af fix osFamily name for sentry, update android-all 2025-08-21 08:00:29 +08:00
Agetian
ba8a30ebdd Ambrosia Whiteheart AI slight improvement (#8473)
* - Add achievement for EOE/EOC by Marek14.

* - Slight logic improvement for Ambrosia Whiteheart
2025-08-20 18:17:35 +03:00
Agetian
e25dbe5196 Add puzzles PS_EOE1 and PS_EOE2. (#8475)
* - Add achievement for EOE/EOC by Marek14.

* - Add puzzles PS_EOE1 and PS_EOE2.
2025-08-20 18:17:11 +03:00
Renato Filipe Vidal Santos
3533ad8b74 Update darigaaz_shivan_champion.txt (#8474) 2025-08-20 18:16:58 +03:00
kevlahnota
2d20ce07e3 Merge pull request #8470 from kevlahnota/master3
move hwinfo output on create method
2025-08-20 21:59:21 +08:00
Anthony Calosa
e5e0a9240d move hwinfo output on create method 2025-08-20 21:55:22 +08:00
kevlahnota
5e83fff859 Merge pull request #8469 from kevlahnota/master3
update device info
2025-08-20 20:58:00 +08:00
Anthony Calosa
2d0acc734e indention 2025-08-20 20:56:22 +08:00
Anthony Calosa
b6bc3a6e96 update device info
should fill up sentry basic device and os info for mobile builds
2025-08-20 20:41:46 +08:00
Eradev
7ec7a65f33 Partially revert 38b6283a by restoring deleted rebalanced card files 2025-08-20 12:55:47 +02:00
Eradev
2a97b67f72 Remove unused import 2025-08-20 12:55:47 +02:00
Eradev
06be01d81c Remove WIP 2025-08-20 12:55:47 +02:00
Eradev
386f6aaac6 Try to get non-Alchemy version 2025-08-20 12:55:47 +02:00
Eradev
82d11bfb45 Update coll.no. for Zendikar Rising 2025-08-20 12:55:47 +02:00
Eradev
f39d900fbf Huge Alchemy Cleanup 2025-08-20 12:55:47 +02:00
Eradev
335edec357 Add info 2025-08-20 12:53:44 +02:00
Eradev
e58f035b19 Update JUD 2025-08-20 12:53:44 +02:00
Eradev
f85d610393 Update J13 2025-08-20 12:53:44 +02:00
Eradev
fce0095af1 Update J12 2025-08-20 12:53:44 +02:00
Eradev
82bea111df Update G11 2025-08-20 12:53:44 +02:00
Eradev
0aded8f1f4 Add borders to H17 2025-08-20 12:53:44 +02:00
Eradev
84130f0586 Update XLN 2025-08-20 12:53:44 +02:00
Eradev
ed4d1059dd Update INV 2025-08-20 12:53:44 +02:00
Eradev
c73be86211 Update ISD 2025-08-20 12:53:44 +02:00
Eradev
6cb2e7a91a Update MID 2025-08-20 12:53:44 +02:00
Eradev
58713e65ed Update VOW 2025-08-20 12:53:44 +02:00
Eradev
676c26fae7 Update IKO 2025-08-20 12:53:44 +02:00
Eradev
f2e4e67021 Update ICE 2025-08-20 12:53:44 +02:00
Eradev
18ba476fc9 Fix H17 2025-08-20 12:53:44 +02:00
Eradev
d9ed82972f Add H17 2025-08-20 12:53:44 +02:00
Eradev
c9affaa1a1 Update HOU 2025-08-20 12:53:44 +02:00
Eradev
8ef89adaef Update HML 2025-08-20 12:53:44 +02:00
Eradev
f25db898ae Remove Code2 MD1 2025-08-20 12:52:52 +02:00
Eradev
cddc41b353 Update MOR 2025-08-20 12:52:52 +02:00
Eradev
d4e918660b Update MMA 2025-08-20 12:52:52 +02:00
Eradev
3f16a3e27f Update MM3 2025-08-20 12:52:52 +02:00
Eradev
53d27f1437 Update MM2 2025-08-20 12:52:52 +02:00
Eradev
408dd310d3 Update MH1 2025-08-20 12:52:52 +02:00
Eradev
0050446ff0 Update MH3 2025-08-20 12:52:52 +02:00
Eradev
efb47d949f Update MH2 2025-08-20 12:52:52 +02:00
Eradev
a67303866e Update MD1 2025-08-20 12:52:52 +02:00
Eradev
756e80595c Update MRD 2025-08-20 12:52:52 +02:00
Eradev
089a021d02 Update PMBS 2025-08-20 12:52:52 +02:00
Eradev
d89a94dcd2 Update MBS 2025-08-20 12:52:52 +02:00
Eradev
b298f66348 Update MIR 2025-08-20 12:52:52 +02:00
Eradev
fa277c7b7c Update MMQ 2025-08-20 12:52:52 +02:00
Eradev
76ec449d33 Update ME1 2025-08-20 12:52:52 +02:00
Eradev
d481ade524 Update ME4 2025-08-20 12:52:52 +02:00
Eradev
1b468efff1 Update ME3 2025-08-20 12:52:52 +02:00
Eradev
9be9a4795b Update ME2 2025-08-20 12:52:52 +02:00
Eradev
49bcf7ded4 Update A25 2025-08-20 12:52:52 +02:00
Eradev
28989b9c49 Update P04 2025-08-20 12:52:52 +02:00
Eradev
9e40b1f6cf Update MPR 2025-08-20 12:52:52 +02:00
Eradev
a9e95caa42 Update ORI 2025-08-20 12:52:52 +02:00
Eradev
eb39c97661 Update TD0 2025-08-20 12:52:52 +02:00
Eradev
52f19272d0 Update M15 2025-08-20 12:52:52 +02:00
Eradev
4cf37379c7 Update PM15 2025-08-20 12:52:52 +02:00
Eradev
746d0476c9 Update PM10 2025-08-20 12:52:52 +02:00
Eradev
cf93d61d0c Update M14 2025-08-20 12:52:52 +02:00
Eradev
5d9197446b Update PM14 2025-08-20 12:52:52 +02:00
Eradev
753d5560d6 Update M13 2025-08-20 12:52:52 +02:00
Eradev
1b68d30ff1 Update PM10 2025-08-20 12:52:52 +02:00
Eradev
278eed7af9 Update PPC1 2025-08-20 12:52:52 +02:00
Hans Mackowiak
6eb4e32225 Update Murders at Karlov Manor.txt 2025-08-20 12:48:52 +02:00
tool4ever
e160a7d517 Tighten scripts (#8467) 2025-08-20 12:34:56 +02:00
Eradev
476cdda0c2 Use KTK goblin as r_1_1_goblin_haste since it has unique art 2025-08-20 12:30:18 +02:00
Eradev
efdabb8bf6 Update PLG25 2025-08-20 12:30:18 +02:00
Eradev
ca648df852 Update LRW 2025-08-20 12:30:18 +02:00
Eradev
04afb61351 Update LEB 2025-08-20 12:30:18 +02:00
Eradev
869632361c Update LEA 2025-08-20 12:30:18 +02:00
Eradev
d994abb559 Update LGN 2025-08-20 12:30:18 +02:00
Eradev
d082c2c250 Update LEG 2025-08-20 12:30:18 +02:00
Eradev
586a474168 Update PZ1 2025-08-20 12:30:18 +02:00
Eradev
b7268dd6fd Update NEO 2025-08-20 12:30:18 +02:00
Eradev
c3e4ea228a Update KTK 2025-08-20 12:30:18 +02:00
Eradev
237946f569 Update KHM 2025-08-20 12:30:18 +02:00
Eradev
cae32e5b2a Update KLD 2025-08-20 12:30:18 +02:00
tool4ever
715ba9803f Fix running out of memory from too many waiting triggers (#8466) 2025-08-20 11:42:04 +02:00
Renato Filipe Vidal Santos
283198f0a4 Tidying trigger-granting Slivers (#8465) 2025-08-20 11:40:54 +02:00
Fulgur14
bab889c406 Firebending cards (TLA) (#8445) 2025-08-20 11:40:22 +02:00
tool4ever
92c6f5369d Fix missing trigger (#8462) 2025-08-20 10:30:55 +02:00
Renato Filipe Vidal Santos
6042948aed Update freyalise_skyshroud_partisan.txt 2025-08-20 06:39:07 +02:00
Fulgur14
136577fec0 Avatar Aang (TLA) (#8459) 2025-08-19 21:30:13 +02:00
tool4ever
180fda53df Update krumar_initiate.txt 2025-08-19 20:03:36 +02:00
Antonio Martinelli
ca6b175fb8 Import from CubeCobra for Draft and Sealed (#8375)
* Import from CubeCobra feature for Draft and Sealed

* adjust comment for cube Import

* CubeImporter refactoring and generalisation

* Allow hyphens in Cube ID validation

* remove unused imports

* make parseFromURL private

* add LAST_IMPORTED_CUBE_ID to ForgePreferences

---------

Co-authored-by: Antonio <mart@gmail.com>
2025-08-19 11:25:50 -04:00
Antonio Martinelli
dd25dc4b06 update rankings_cubecobra.txt (#8458) 2025-08-19 18:13:08 +03:00
Hans Mackowiak
9893fc3cf9 Player: extra Flag for Element Bend 2025-08-19 16:41:31 +02:00
Michael Rappazzo
67b6c2c03f fix: move CHANGES.txt generation to target directory
- Generate CHANGES.txt in forge-gui-desktop/target/ instead of source tree
- Update installer to copy from target directory for all build profiles
- Add CHANGES.txt to .gitignore since it's generated
- Remove hardcoded fromRef to use latest tag automatically
- Remove maven-release-plugin exclusion for untracked file
2025-08-19 06:49:42 -04:00
Agetian
bb073b8682 Tweak AI logic for Krumar Initiate (#8453)
* - Add achievement for EOE/EOC by Marek14.

* - Add AI logic for Krumar Initiate

* - Tweak AI logic for Krumar Initiate

* Update EndureAi.java

only when doing X

---------

Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-08-19 13:36:11 +03:00
Agetian
9bbfad5be3 AI logic for Krumar Initiate (#8450)
* - Add achievement for EOE/EOC by Marek14.

* - Add AI logic for Krumar Initiate
2025-08-19 13:11:41 +03:00
Paul Hammerton
2c7e7d5b63 Merge pull request #8448 from paulsnoops/edition-updates
Edition updates: AA1, AA2
2025-08-19 10:21:44 +01:00
Paul Hammerton
94e7258a03 Edition updates: AA1, AA2 2025-08-19 10:18:58 +01:00
Renato Filipe Vidal Santos
95eb90eac2 Update claws_out.txt 2025-08-19 07:08:24 +02:00
Eradev
0f0a4c54e7 Fix Hanweir Garrison CollNo 2025-08-19 07:07:27 +02:00
Eradev
399923bfa4 Remove dupe + fix SIR 2025-08-19 07:07:27 +02:00
Eradev
39f0ab9eae Fix closing bracket 2025-08-19 07:07:27 +02:00
Eradev
8b1cd54417 Search split types 2025-08-19 07:07:27 +02:00
Eradev
ff2192fa7a Fix Meld key 2025-08-19 07:07:27 +02:00
Eradev
08443a307c Fix Domri Rade's emblem 2025-08-19 06:35:36 +02:00
Eradev
bd90e1bccc Check for emblems in other instead of tokens 2025-08-19 06:35:36 +02:00
Hans Mackowiak
9cb0bd301e TLA: Firebending 2025-08-19 06:13:00 +02:00
kevlahnota
0923960215 Merge pull request #8442 from Jetz72/fixes20250818
Fix auto-sell cards reappearing in collection
2025-08-19 08:36:26 +08:00
Jetz
0f68dc1ab6 Make give item command case-insensitive 2025-08-18 19:06:52 -04:00
Jetz
09b88b8575 Fix auto-sell not being removed from adventure collection 2025-08-18 19:05:02 -04:00
kevlahnota
fae2f25b69 Merge pull request #8441 from kevlahnota/master3
update Itemmanager portrait layout, update Sentry scope
2025-08-19 06:29:03 +08:00
Anthony Calosa
d3e0696ecc update Itemmanager portrait layout, update Sentry scope 2025-08-19 06:19:44 +08:00
Paul Hammerton
6a7723eba9 Merge pull request #8440 from paulsnoops/fix-scripts
Fix Earth Rumble and Roku's Mastery
2025-08-18 23:05:34 +01:00
Paul Hammerton
10d6fd157e Fix Earth Rumble and Roku's Mastery 2025-08-18 23:01:10 +01:00
Paul Hammerton
048133df30 Merge pull request #8439 from paulsnoops/aa2-edition-fixes
AA2 edition spelling
2025-08-18 22:56:04 +01:00
Paul Hammerton
2fe7fec14f AA2 edition spelling 2025-08-18 22:53:36 +01:00
Paul Hammerton
73e7b27c09 Merge pull request #8438 from paulsnoops/yeoe-formats
Format updates: YEOE
2025-08-18 22:35:33 +01:00
Paul Hammerton
2fdbd5a85c Add YEOE to formats 2025-08-18 22:32:06 +01:00
Paul Hammerton
78e27e0073 Merge pull request #8437 from paulsnoops/edition-updates
Edition updates: AA1, AA2, SLD, TLA, TLE, YEOE
2025-08-18 22:25:38 +01:00
Paul Hammerton
bad585c8c9 Edition updates: AA1, AA2, SLD, TLA, TLE, YEOE 2025-08-18 22:23:11 +01:00
Eradev
11d10a8129 Fix Meld cards fetch (#7981)
* BOM

* ANB Update

* J25 Update

* SIR update

* Mend

* Find the right back for meld cards

* ANB Update

* J25 Update

* SIR update

* Mend

* Find the right back for meld cards

* Update PW25

* Update SPG

* Typo

* Update ACR

* Update CMM

* Add PFDN and Invasion alternate arts

* Update MKM

* Update NPH

* J25 -> J22 for non-existing cards

* Update WOC

* Update HBG

* Use printsheet

* Use helper functions
2025-08-18 14:31:33 +02:00
Agetian
0b89e9d137 - Add achievement for EOE/EOC by Marek14. (#8433) 2025-08-18 15:21:09 +03:00
kevlahnota
70df1ff0aa Merge pull request #8428 from Jetz72/fixes20250817
Handful of Deck Editor updates
2025-08-18 16:52:30 +08:00
Jetz
24d3169592 Enable variant replacement in Quest and Planar Conquest 2025-08-17 21:37:36 -04:00
Jetz
b8db0fea5e Disable custom cards conformity check in Adventure mode. 2025-08-17 21:11:51 -04:00
Jetz
514519b45b Merge branch 'master' into fixes20250817
# Conflicts:
#	forge-gui-mobile/src/forge/itemmanager/ItemManager.java
2025-08-17 21:01:42 -04:00
Jetz
70f6bcb63c Fix missing or crushed group and pile-by buttons in narrow layouts 2025-08-17 20:52:21 -04:00
Jetz
5a3e55f704 Fix crushed filter buttons in portrait mode 2025-08-17 20:51:20 -04:00
Jetz
4452e07443 Remove unused format filter visibility controls 2025-08-17 19:58:20 -04:00
Jetz
a125e6c0ea Support command line args width= and height= for easier testing of portrait layouts. 2025-08-17 19:57:21 -04:00
kevlahnota
fa4688e113 Merge pull request #8426 from kevlahnota/master3
fix Itemmanager portrait layout
2025-08-18 07:46:12 +08:00
Anthony Calosa
0236609558 fix Itemmanager portrait layout 2025-08-18 07:40:52 +08:00
kevlahnota
3f15fb1b98 Merge pull request #8424 from shenshinoman/Crystal_Expansions
Fixing bugs that were preventing proper world gen in Crystal Kingdoms…
2025-08-18 06:01:41 +08:00
kevlahnota
e22718c72a Merge pull request #8425 from kevlahnota/master3
fix RandomDeckGenerator AI Deck Selection by preference
2025-08-18 05:59:47 +08:00
Anthony Calosa
e94985576c refactor check 2025-08-18 05:56:54 +08:00
Anthony Calosa
71e132a9e0 fix RandomDeckGenerator AI Deck Selection by preference 2025-08-18 05:52:24 +08:00
Jetz
8d5d56bed8 Add dev cheat to add cards to inventory and draft packs 2025-08-17 17:18:17 -04:00
shenshinoman
386c9799c2 Fixing bugs that were preventing proper world gen in Crystal Kingdoms. Very basic fix, and a slight notice in the quests to inform the player that they have not been udpated yet. 2025-08-17 16:08:03 -04:00
Fulgur14
a1be657460 Airbending cards (#8422) 2025-08-17 19:36:15 +01:00
Fulgur14
d4994e8fa0 Fire Lord Ozai (TLE) (#8361)
* Support CombatMana
2025-08-17 17:03:26 +01:00
Renato Filipe Vidal Santos
fb054d9f64 Tidying DBCleanup (#8420) 2025-08-17 13:34:23 +01:00
kevlahnota
8790843b3b Merge pull request #8421 from kevlahnota/master3
revert fileprovider, add sentryscope
2025-08-17 20:32:51 +08:00
Anthony Calosa
38383dff02 revert fileprovider, add sentryscope 2025-08-17 20:20:33 +08:00
Hans Mackowiak
a0f9923c21 TLA: Airbend (#8419) 2025-08-17 13:24:11 +02:00
kevlahnota
812c0ac5b1 Merge pull request #8418 from kevlahnota/master3
update SettingsScene, Settings page indent
2025-08-17 11:12:43 +08:00
Anthony Calosa
2e09146f44 remove unused options 2025-08-17 11:02:48 +08:00
Anthony Calosa
25080411d5 update SettingsScene, Settings page indent
limit hasprintedpt to vehicles and spacecraft to fix modal card renders with non creature backside
- closes #8413
2025-08-17 10:52:17 +08:00
kevlahnota
55e20e96f9 Merge pull request #8414 from Jetz72/fixes20250816
Attempt to fix another NPE in deserializing decks
2025-08-17 06:14:31 +08:00
kevlahnota
981ab1c6ed prevent NPE 2025-08-17 06:13:51 +08:00
Hans Mackowiak
a1089f8073 Update the_cabbage_merchant.txt 2025-08-16 23:56:03 +02:00
tool4ever
fe747a3908 Update the_cabbage_merchant.txt 2025-08-16 21:50:39 +02:00
Paul Hammerton
a16c9480e7 Merge pull request #8412 from Eradev/FixEOE
Fix EOE def
2025-08-16 16:21:41 +01:00
Jetz72
b40e9fc817 Merge branch 'Card-Forge:master' into fixes20250816 2025-08-16 10:15:33 -05:00
Jetz
6eec0e8988 Fix another potential NPE deserializing old decks... 2025-08-16 11:13:42 -04:00
Hans Mackowiak
d6320caadf CardEdition: getPrintSheetsBySection use collectorNumber instead of index 2025-08-16 16:53:43 +02:00
Eradev
c4f125525a Fix EOE def 2025-08-16 10:38:43 -04:00
kevlahnota
56209261d0 Merge pull request #8411 from Jetz72/fixes20250816
Fix NPE deserializing old decks
2025-08-16 22:37:44 +08:00
Jetz
5df4d64345 Fix a potential NPE deserializing old decks.
Fix `Deck.cloneFieldsTo` to copy tags in the right direction.
2025-08-16 10:05:41 -04:00
kevlahnota
d6d6065104 Merge pull request #8402 from Jetz72/fixes20250815c
A few auto-sell section fixes
2025-08-16 14:32:04 +08:00
kevlahnota
333aea0641 Merge pull request #8405 from kevlahnota/master3
fix booster draft menu not showing on first try
2025-08-16 14:29:57 +08:00
Anthony Calosa
c7bb3d49a6 fix booster draft menu not showing on first try 2025-08-16 14:14:33 +08:00
Jetz
7c982498dd Avoid trying to generate a move option for cards that don't have a valid destination 2025-08-15 23:51:19 -04:00
Jetz
0e2c95afed Fix ClassCastException 2025-08-15 23:48:50 -04:00
Jetz
df6eb23341 Always show quantity of auto-sell section. 2025-08-15 23:48:31 -04:00
kevlahnota
1204de13c3 Merge pull request #8400 from Jetz72/fixes20250815b
Fix negative gold payouts from selling cards.
2025-08-16 09:55:23 +08:00
Jetz
9cb21de11d Fix negative gold payouts from selling cards. 2025-08-15 21:37:58 -04:00
kevlahnota
2f16b9e110 Merge pull request #8399 from Jetz72/fixes20250815
Fix some crashes from the new editor
2025-08-16 07:22:57 +08:00
Jetz
cf28139b02 Fix a second crash for the same reason... 2025-08-15 19:19:28 -04:00
Jetz
87e8e7d5e5 Fix crash entering a game without extra sections 2025-08-15 19:12:40 -04:00
Jetz
dcc3a681d9 Whoops committed the wrong file.
This reverts commit cc7f30da88.
2025-08-15 19:12:39 -04:00
Jetz
cc7f30da88 Fix crash entering a game without extra sections 2025-08-15 19:05:59 -04:00
Jetz
ed9eabed38 Fix Jumpstart deck preview 2025-08-15 18:31:45 -04:00
Paul Hammerton
99ac95bfca Merge pull request #8398 from paulsnoops/edition-updates
Edition updates: PMEI
2025-08-15 21:24:05 +01:00
Paul Hammerton
eb178fc9d1 Edition updates: PMEI 2025-08-15 21:15:07 +01:00
Paul Hammerton
7567e29cf1 Merge pull request #8397 from paulsnoops/edition-updates
Edition updates: TLA, TLE, YEOE
2025-08-15 19:18:48 +01:00
Paul Hammerton
b2a456140c YEOE 2025-08-15 19:11:49 +01:00
Paul Hammerton
9c3ff6b570 Edition updates: TLA, TLE 2025-08-15 18:40:44 +01:00
Hans Mackowiak
e4d238ba6c ~lf 2025-08-15 19:27:29 +02:00
Hans Mackowiak
42363204dd add PrintSheet & ItemPool helper 2025-08-15 19:27:09 +02:00
Jetz72
de89d557dd Mobile Deck Editor Reorganization and Feature Update (#7519)
Mobile Deck Editor Reorg
2025-08-15 11:48:25 -04:00
kevlahnota
c5a53b21e7 Merge pull request #8396 from kevlahnota/master3
fix flip cards redownloading same image with different name
2025-08-15 20:43:07 +08:00
Anthony Calosa
02f43b1ef0 update comment 2025-08-15 20:37:58 +08:00
Anthony Calosa
d7cb0b7ac1 fix flip cards redownloading same image with different name
also fix sync for sentry native ndk since this is declared as system scope
2025-08-15 20:34:26 +08:00
kevlahnota
2efeb573e6 Merge pull request #8395 from Eradev/DisplayFlipCards
Display flip cards correctly
2025-08-15 20:10:24 +08:00
kevlahnota
370ff638ab Merge pull request #8394 from dracontes/rv-fix_odin
Update summon_primal_odin.txt
2025-08-15 20:10:10 +08:00
kevlahnota
108564efe3 Merge pull request #8392 from Eradev/UpdatePLST
Update PLST
2025-08-15 20:09:34 +08:00
Eradev
43f96657e0 Rename vars 2025-08-15 06:57:06 -04:00
Eradev
cb004bfba2 Fix imports 2025-08-15 06:53:17 -04:00
Eradev
f276597a82 Flip in Reward screen 2025-08-15 06:49:18 -04:00
Renato Filipe Vidal Santos
4fd9855daa Update summon_primal_odin.txt 2025-08-15 11:27:31 +01:00
Eradev
ebdce74f92 Fix display in inventory 2025-08-15 06:20:20 -04:00
Eradev
98e9b8f28b Display flipped cards correctly 2025-08-15 05:54:31 -04:00
Hans Mackowiak
a31c0358a4 Update TriggerType.java
Add the other Bending trigger
2025-08-15 11:21:22 +02:00
Fulgur14
8640522fff TLA/TLE cards (14th August), Batch 4 (#8387) 2025-08-15 09:59:17 +02:00
Fulgur14
a5fa350e7e TLA/TLE cards (14th August), Batch 3 (#8385) 2025-08-15 09:57:34 +02:00
Fulgur14
c4a0cff1ca TLA/TLE cards (14th August), Batch 2 (#8383) 2025-08-15 09:56:22 +02:00
Fulgur14
30d02ebbb6 TLA/TLE cards (14th August), Batch 1 (#8382) 2025-08-15 09:48:11 +02:00
Hans Mackowiak
9a90359283 Update EarthbendEffect.java
Fix apostrophe
2025-08-15 09:44:55 +02:00
Fulgur14
edbcab544e TLA/TLE Earthbending cards (#8393) 2025-08-15 09:42:48 +02:00
Hans Mackowiak
f55bf4691d TLA: Earthbend (#8386) 2025-08-15 09:24:45 +02:00
Eradev
16e44309a6 Update PLST 2025-08-15 02:43:27 -04:00
tool4ever
c4d58e3dba Update wormfang_drake.txt 2025-08-15 08:33:17 +02:00
Ellios77
16155d3670 Punctuation fixes for card scripts (#8389) 2025-08-15 08:21:49 +02:00
kevlahnota
bd929bcc72 Merge pull request #8388 from kevlahnota/master3
add Preload Custom Drafts
2025-08-15 08:52:49 +08:00
Anthony Calosa
e2cc52fd02 add Preload Custom Drafts
Should fix longer startup time on mobile version instead of forcing to load custom drafts file at startup.
2025-08-15 08:45:21 +08:00
Paul Hammerton
be5c7cfd04 Merge pull request #8381 from paulsnoops/unf-update
Edition updates: UNF
2025-08-14 18:27:57 +01:00
Paul Hammerton
9d14949138 Edition updates: UNF 2025-08-14 18:25:28 +01:00
tool4ever
6fe5dbad3c Update InputAttack.java 2025-08-14 16:53:18 +00:00
Matthew Scott Krafczyk
f4f8ee9cb6 android-dev-build (#8352)
* improve resource finding procedure and add android-dev-build

Added the `android-dev-build` profile to trigger building a developer
apk.

Developer apk is at application id forge.app.dev, and has name
'Forge (dev)'. It is installable in parallel with the official release
app allowing developers to test the android build on their phones
without interfering with their existing installation.

Updated how android resources are found to accomodate changes in
application id at runtime. This method doesn't rely on the 'R' package.

* Use all arguments of getIdentifier

Use all arguments of getIdentifier instead of using a fully qualified
resource name
2025-08-14 19:06:54 +03:00
Antonio Martinelli
0767eb03e0 Add LSVCube and Name-Sticker Goblin (card) (#8348)
* Add LSVCube and draft

* Add "Name Sticker" Goblin card (MTGO Version)

---------

Co-authored-by: Antonio <mart@gmail.com>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-08-14 19:06:46 +03:00
kevlahnota
feb6062f2f Update MakeCardEffect.java
- closes #8373
2025-08-14 22:44:23 +08:00
Eradev
113dc4d5e8 Don't try to attack itself. (#8376) 2025-08-14 16:11:00 +02:00
Hans Mackowiak
ca20d98a2c Update Tarkir Dragonstorm.txt 2025-08-14 14:15:00 +02:00
Hans Mackowiak
599a629068 Update Dungeons & Dragons Adventures in the Forgotten Realms.txt
Also add Dungeons to "other" section
2025-08-14 09:54:59 +02:00
Hans Mackowiak
4c05ad655d Update Dungeons & Dragons Adventures in the Forgotten Realms.txt
Fix Dungeon CN
2025-08-14 09:22:13 +02:00
Ellios77
203233f0d2 Fixed typo in script (#8372) 2025-08-14 09:18:31 +02:00
Hans Mackowiak
ac8f7357db Update corrupted_shapeshifter.txt 2025-08-14 06:29:33 +02:00
kevlahnota
4c396065f5 Merge pull request #8370 from kevlahnota/master3
update Sentry
2025-08-14 06:07:54 +08:00
Anthony Calosa
cc4dac38fe update Sentry 2025-08-14 05:59:47 +08:00
Eradev
394818f533 Filter by foil status (#8358) 2025-08-13 19:45:53 +03:00
Eradev
bfd4a68e23 Try to attack mustAttackEntities first. (#8355) 2025-08-13 19:45:42 +03:00
Paul Hammerton
9d54e82214 Merge pull request #8368 from paulsnoops/edition-updates
Edition updates: TLA, TLE
2025-08-13 17:41:30 +01:00
Paul Hammerton
18c2278066 Edition updates: TLA, TLE 2025-08-13 17:40:00 +01:00
tool4ever
b3d549a712 Update mirror_shield_hoplite.txt 2025-08-13 16:22:58 +00:00
tool4ever
0887e78e90 Update agrus_kos_eternal_soldier.txt 2025-08-13 16:22:26 +00:00
Hans Mackowiak
df3f86dd73 CardState: Fix Card copies itself though ReplacementEffect (#8356)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-13 17:51:14 +02:00
Hans Mackowiak
89af1d0cf1 Update Card.java 2025-08-13 17:07:32 +02:00
Eradev
10a68d63a1 Fix exact search (#8357) 2025-08-13 08:46:47 +00:00
Hans Mackowiak
8ccebbc13c Update The List.txt
Closes #8363
2025-08-13 10:37:22 +02:00
Fulgur14
4c663cd943 TLA/TLE cards that don't require bending (#8359) 2025-08-13 08:36:41 +00:00
Hans Mackowiak
ca16bf74f5 Update Card.hasNoAbilities
For Adventure and Omen
2025-08-13 10:09:01 +02:00
tool4ever
a1968ef9fd Update depthshaker_titan.txt 2025-08-13 07:58:25 +02:00
kevlahnota
de1a999611 Merge pull request #8353 from kevlahnota/master3
exclude unused xcf files in releases
2025-08-12 20:56:41 +08:00
Anthony Calosa
d0e8bc5de0 exclude unused xcf files in releases
- closes #8322
2025-08-12 20:49:49 +08:00
Eradev
38fb647cd7 Migration to PLST (#7972)
* Fallback to PLST

* Fix audit for Funny cards.

* Remove FMB1, update PLST

* Fallback to PLST

* Convert cards to PLST

* BOM

* Minor fix

* LF

* Remove conversion. Already handled.
2025-08-12 08:17:11 +03:00
kevlahnota
90bb83c5d4 Merge pull request #8351 from kevlahnota/master3
add backup/restore classic mode data
2025-08-11 22:00:07 +08:00
Anthony Calosa
467fff3651 add backup/restore classic mode data 2025-08-11 21:53:19 +08:00
Hans Mackowiak
3ab50a5fb5 Update samis_curiosity.txt
Add StackDesc for Lander Token
2025-08-11 09:35:03 +02:00
Tim Miller
e4d58a0a88 Add other deck sections for other formats 2025-08-10 17:03:30 -04:00
kevlahnota
10e592c741 Merge pull request #8343 from kevlahnota/master3
request external storage access
2025-08-10 20:04:33 +08:00
Anthony Calosa
b3241c85eb request external storage access
- closes #7405
2025-08-10 19:56:29 +08:00
Cees Timmerman
abd1702c96 Fix NullPointerException in forge.game.zone.Zone.toString (#8326) (#8327) 2025-08-10 08:03:56 +02:00
Hans Mackowiak
d91e86c608 Update GameEventManaPool.java (#8340)
Closes #8328, #8329
2025-08-09 16:13:49 +02:00
Hans Mackowiak
f0ebac019d Perpetual: use Record for different types (#8337)
* Perpetual: use Record for different types
2025-08-09 13:04:14 +02:00
tool4ever
c18995915a Fix scripts (#8339) 2025-08-09 10:50:27 +00:00
Paul Hammerton
55d2c23aa0 Merge pull request #8338 from paulsnoops/edition-updates
Edition updates: PSPM, PW25, SLD, SPM
2025-08-09 10:57:58 +01:00
Paul Hammerton
28c9f719c5 Edition updates: PSPM, PW25, SLD, SPM 2025-08-09 10:56:08 +01:00
Hans Mackowiak
bb5a276324 Update kessig.txt
Fix ActiveZones
2025-08-09 08:51:13 +02:00
tool4ever
e99df5b267 Update wellgabber_apothecary.txt
Closes #8334
2025-08-08 18:07:11 +00:00
kevlahnota
54e67b3d57 Merge pull request #8333 from kevlahnota/master3
update progressbar, update boosterdraft readfile
2025-08-08 22:01:42 +08:00
Anthony Calosa
b1d30ab9dd remove unused borderless list 2025-08-08 21:31:32 +08:00
Anthony Calosa
f2d65b2515 update check 2025-08-08 21:01:58 +08:00
Anthony Calosa
286fcd3fbe update progressbar, update boosterdraft readfile
also removed unneccesary settings since forge uses shaders for round corners for rendering borders
2025-08-08 20:46:12 +08:00
Hans Mackowiak
00332ff3fe Fix Phantom Train not updating PT (#8332)
* Fix Phantom Train not updating PT
2025-08-08 12:52:29 +02:00
tool4ever
000fa14c17 Fix voting crash (#8325) 2025-08-08 10:15:27 +02:00
tool4ever
c5b4be6c97 Update edgar_master_machinist.txt 2025-08-07 16:13:52 +00:00
kevlahnota
d9b2a8f4e0 Merge pull request #8321 from kevlahnota/master3
update sentry
2025-08-07 21:50:08 +08:00
Anthony Calosa
2898b00300 update sentry 2025-08-07 21:42:20 +08:00
Hans Mackowiak
a4671b62d4 CardStateName: combine Modal with Transformed (#8305)
* CardStateName: combine Modal with Transformed

* Make new TMDFC transformable

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-08-07 11:28:24 +02:00
kevlahnota
0c87ed7381 Merge pull request #8319 from kevlahnota/master3
fix VPrompt
2025-08-07 05:29:34 +08:00
Anthony Calosa
4104a0b2a8 fix VPrompt 2025-08-07 05:28:13 +08:00
tool4ever
4d62f436d5 ExiledWith check for keyworded (#8317) 2025-08-06 19:41:39 +00:00
kevlahnota
506f88bd6f Update friendly_neighborhood.txt
fix no API
2025-08-06 20:47:32 +08:00
Hans Mackowiak
a1ad355e25 Update CardEdition.java
Closes #8314 8314
2025-08-06 14:17:13 +02:00
kevlahnota
91d6571cff Merge pull request #8315 from kevlahnota/master3
Reversed prompt button toggle
2025-08-06 18:57:55 +08:00
Anthony Calosa
e43c5534b3 Reversed prompt button toggle
- closes #7366
2025-08-06 18:51:49 +08:00
Jetz
77e3453d35 Fix vote out card type 2025-08-05 21:50:39 -04:00
Jetz
2568df1b91 Reorder ReplaceMoved.canReplace to do some common easy checks first. 2025-08-05 21:50:39 -04:00
Jetz
dc830a8432 Include weird zones in forEachCardInGame 2025-08-05 21:50:39 -04:00
Chris H
a9cffbd313 Collect all of slots paper cards before adding it to the final result 2025-08-05 21:05:49 -04:00
tool4ever
65775f5c20 Update moonlit_meditation.txt
Closes #8310
2025-08-05 20:00:03 +00:00
Fulgur14
c6f9056e2c Gwen and Miles (Transformation to be added later) (#8306)
* Gwen and Miles (Transformation to be added later)

* Create miles_morales_ultimate_spider_man.txt

* Update miles_morales_ultimate_spider_man.txt

* Update miles_morales_ultimate_spider_man.txt
2025-08-05 19:06:01 +03:00
Hans Mackowiak
967807d441 Merge pull request #8307
Fixes Vote Protection
2025-08-05 11:17:15 +02:00
Hans Mackowiak
cc7b3505d7 Update ComputerUtilCard.java 2025-08-05 09:54:43 +02:00
Hans Mackowiak
3cd965a24f Update ComputerUtilCard.java 2025-08-05 09:49:11 +02:00
Hans Mackowiak
3502bf6de2 Update ComputerUtil.java
Fix
2025-08-05 09:45:31 +02:00
Hans Mackowiak
458e05635c Update CardFactoryUtil.java
Allow Iterable for getMostProminentColorsFromList
2025-08-05 09:42:47 +02:00
Hans Mackowiak
f4cd9e0f4f Update ComputerUtil.java
Fix Vote "Protection"
2025-08-05 09:40:12 +02:00
tool4EvEr
1f3871d5dc Thread.stop fallback 2025-08-04 22:15:23 +02:00
tool4ever
f4d304b2c1 Update abyssal_hunter.txt 2025-08-04 13:00:15 +00:00
tool4ever
24937fb32d Reduce potential for ConcurrentModificationException (#8302) 2025-08-04 14:30:10 +02:00
Hans Mackowiak
5d222577be Update AddTurnAi.java
Fix AddTurnAi
2025-08-04 14:15:46 +02:00
Hans Mackowiak
0e010cece2 Update ComputerUtil.java
Fix vote "StrengthOrNumbers"
2025-08-04 14:15:02 +02:00
Hans Mackowiak
21080d65f5 Create b_0_0_germ.txt
Closes #8301
2025-08-04 13:20:47 +02:00
Fulgur14
83475b9063 Carnage, Crimson Chaos (SPM) (#8299) 2025-08-04 07:40:39 +02:00
tool4ever
15a6494d9c Update space_time_anomaly.txt
Closes #8297
2025-08-03 16:26:40 +00:00
Hans Mackowiak
58ad1f3103 Keyword: Web-slinging 2025-08-03 16:24:52 +02:00
Hans Mackowiak
b2bff7762f Mayhem: add new Cast-from-Graveyard variant 2025-08-03 16:24:24 +02:00
kevlahnota
cd4a673862 Merge pull request #8295 from kevlahnota/master3
fix NPE and FChoiceList render display
2025-08-03 20:54:13 +08:00
Anthony Calosa
1f61c6bd00 fix NPE and FChoiceList render display 2025-08-03 20:51:02 +08:00
Cees Timmerman
cc2f46795f Fix infinite loop OOM crash (#8254) 2025-08-02 15:15:49 +02:00
Chris H
db5ea3ec9c Fix undo restore restoring to bottom of library 2025-08-02 15:03:27 +02:00
Adam Jones
21f2f6e2c3 Update forge.sh
Allow parameters to be passed in to the shell script
2025-08-02 15:03:11 +02:00
Hans Mackowiak
65b5f4fac3 VoteEffect: use SpellAbility Choices (#8286)
* VoteEffect: use SpellAbility Choices

* fixes

* fix VotePlayer

* subAbs only for Choices
2025-08-02 16:01:12 +03:00
tool4ever
2291cf9d0c Fix scripts (#8288) 2025-08-02 11:29:01 +00:00
Paul Hammerton
3f8b3fa2a7 Merge pull request #8287 from paulsnoops/edition-updates
Edition updates: PW25, SLD, SPM
2025-08-02 10:24:59 +01:00
Paul Hammerton
e6fa1268d9 Edition updates: PW25, SLD, SPM 2025-08-02 10:23:36 +01:00
Renato Filipe Vidal Santos
eda8205acd Fixing Ultima, Origin of Oblivion (#8278)
* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt

* Update ultima_origin_of_oblivion.txt
2025-08-02 08:20:50 +03:00
Renato Filipe Vidal Santos
019131447a Update icetill_explorer.txt 2025-08-02 06:59:59 +02:00
kevlahnota
41cd90bc2a Fix dual foil renders 2025-08-02 00:38:03 +08:00
kevlahnota
0fe1677ced Update Foil rotation check 2025-08-02 00:27:00 +08:00
tool4ever
b1a2a93f75 Update PumpAllAi.java 2025-08-01 12:43:56 +02:00
tool4ever
50a2c283f1 Update lightstall_inquisitor.txt 2025-08-01 08:34:25 +02:00
kevlahnota
83188daba4 Merge pull request #8275 from Card-Forge/kevlahnota-rotatefix
Extra check for rotation
2025-08-01 08:31:50 +08:00
kevlahnota
3312e409fd Extra check for rotation 2025-08-01 08:24:38 +08:00
tool4ever
5420a90f05 Update iron_spider_stark_upgrade.txt
Closes #8272
2025-07-31 20:16:28 +00:00
tool4ever
4d340d7bf3 Update wrathful_raptors.txt 2025-07-31 20:15:06 +00:00
tool4ever
5342175a97 Update wrathful_red_dragon.txt 2025-07-31 20:14:39 +00:00
tool4ever
e5a1c86b1f Align invalid Player property result with Card (for Lozhan) (#8264) 2025-07-31 18:56:55 +00:00
tool4ever
1aca32005b Fix Brave the Wilds (#8266)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-31 21:32:12 +03:00
tool4ever
6d7853428f Fix bad refactor (for real this time) (#8263)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-31 20:05:05 +02:00
kevlahnota
683d92399d FDeckEditor Foil Menu (#8260) 2025-07-31 21:58:05 +08:00
kevlahnota
9b9bf6cf53 update holofoil texture (#8259)
a bit modern but has more blended effect with most card images along with hue changes effect
2025-07-31 20:24:55 +08:00
kevlahnota
c9b826d9b4 Merge pull request #8237 from kevlahnota/master2
Refactor Foil Effect for Mobile
2025-07-31 05:48:36 +08:00
kevlahnota
81ff72f1f2 Merge pull request #8189 from kevlahnota/nyxDesktop
add nyx effect to desktop card renders
2025-07-31 05:45:46 +08:00
tool4ever
dfda04f7ba Fix bad refactor (#8252) 2025-07-30 20:43:20 +00:00
tool4ever
3bc0e0c386 More AI cleanup (#8251)
* Fix infinite loop
2025-07-30 19:59:20 +00:00
Hans Mackowiak
5829192446 Update valgavoths_onslaught.txt
fix SpellDescription
2025-07-30 21:50:57 +02:00
Chris H
cbae45be87 Update shaun_father_of_synths.txt (#8250) 2025-07-30 19:23:54 +00:00
Agetian
2b4ec73318 - Tweak marked damage on PS_FIN4. (#8249) 2025-07-30 18:21:59 +03:00
Agetian
7ff426e413 - Add puzzle PS_FIN4. (#8248) 2025-07-30 18:19:26 +03:00
shenshinoman
25e3ad8432 Resolving the land shop issue in Shandalar Old World. (#8231) 2025-07-30 17:52:25 +03:00
tool4ever
051edff18e Consolidate logic (#8247) 2025-07-30 09:50:59 +00:00
tool4ever
dde58be5a6 Clean up redundant "AI" from function names (#8243) 2025-07-30 07:44:17 +00:00
lemtom
1ad4fda3e6 16x16 avatars for every enemy (#8206)
* 16x16 avatars for every enemy

* 16x16 death knight

* 16x16 zombie poisoner

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-07-30 07:50:51 +03:00
shenshinoman
b78ef7b010 Updating the "InsectShop" shop icon to a newly provided one from the community. Icon provided by coby_cat from the discord. (Also updated the GIMP XCF project file to match the current png. So as to ensure those using GIMP to edit these are up to date.) (#8227) 2025-07-30 07:50:41 +03:00
Renato Filipe Vidal Santos
d1776ce37a Add files via upload (#8205) 2025-07-30 07:50:32 +03:00
Anthony Calosa
100ae7b164 update dispose 2025-07-30 08:51:32 +08:00
tool4ever
415e247f6c Cleanup API handling Part 2 (#8241)
* Improve entrypoints

* Consolidate AtOppEOT
2025-07-29 19:12:52 +00:00
tool4ever
ca4a934e0f Cleanup API handling (#8238)
* Remove obsolete branch

* Merge ward logic

* Convert Fatal Push logic

* Streamline condition handling!

* Consolidate AIPlayForSub logic

* call preventRunAwayActivations earlier in Template

* Consolidate some logic

* Update LifeGainAi.java

* Update SurveilAi.java

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-07-29 16:11:03 +02:00
Anthony Calosa
3b4c417549 Refactor Foil Effect for Mobile
Should fix rendering for Full, Crop and Art renders
2025-07-29 19:42:01 +08:00
tool4ever
f5e96bc756 Streamline logic so canPlay always before canPay (#8234) 2025-07-29 07:35:18 +00:00
Chris H
7428b7420a Update dyadrine_synthesis_amalgam.txt (#8232) 2025-07-29 06:56:26 +02:00
kevlahnota
761d7848ef Merge pull request #8233 from kevlahnota/master2
fix SwingImageFetcher filename
2025-07-29 10:46:29 +08:00
Anthony Calosa
e9fc7899cd fix SwingImageFetcher filename 2025-07-29 10:44:57 +08:00
Chris H
df40fd5cb3 Convert checkApiLogic to AiAbilityDecision 2025-07-28 15:43:50 -04:00
Chris H
dce3b4b142 Thoguht i fixed this one already 2025-07-28 15:43:50 -04:00
Chris H
b20110edb4 Code review updatse 2025-07-28 15:43:50 -04:00
Chris H
4a05a66bc0 Update forge-ai/src/main/java/forge/ai/ability/VentureAi.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-07-28 15:43:50 -04:00
Chris H
39acc78fad Convert AiAbilityDecision to a record 2025-07-28 15:43:50 -04:00
Chris H
a0c58e2a1d Convert booleans to AiAbilityDecision objects to aid in decision making 2025-07-28 15:43:50 -04:00
Hans Mackowiak
c46ceca140 Update count_on_luck.txt
Remove wrong PT
2025-07-28 15:45:59 +02:00
tool4ever
4db0b6a67f Trigger fix (#8226) 2025-07-28 10:17:18 +02:00
tool4ever
38f6b699c6 Fix ward calculation when same card is targeted multiple times (#8222)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-28 07:36:36 +03:00
Paul Hammerton
9d80f4e78f Merge pull request #8221 from paulsnoops/edition-updates
Edition updates: TLA
2025-07-27 10:14:33 +01:00
Paul Hammerton
7dc95f8540 Edition updates: TLA 2025-07-27 10:13:32 +01:00
tool4ever
723ad5fe7c Update loyal_unicorn.txt 2025-07-27 08:17:56 +00:00
tool4ever
94aac494ea Update loyal_unicorn.txt 2025-07-27 08:17:25 +00:00
tool4ever
3e6b7af61f Update shaun_and_rebecca_agents.txt 2025-07-27 07:13:01 +00:00
Fulgur14
1574a88255 Create kravens_last_hunt.txt (#8220) 2025-07-27 07:11:39 +00:00
Renato Filipe Vidal Santos
2e2fea9182 SPM: 8 cards 2025-07-26 20:24:30 +00:00
kevlahnota
55549f1e70 Merge pull request #8218 from kevlahnota/master2
fix DualListBox Scrollbounds width and height
2025-07-26 21:56:21 +08:00
Anthony Calosa
71e97146ed fix DualListBox Scrollbounds width and height 2025-07-26 21:55:17 +08:00
tool4ever
35e4e580fa Cleanup duplicated field (#8217) 2025-07-26 12:09:09 +00:00
tool4ever
baab31d2e2 Rework Shared Fate (#8215) 2025-07-26 10:04:17 +00:00
Paul Hammerton
3f1295d9e4 Merge pull request #8216 from paulsnoops/edition-updates
Edition updates: SPM
2025-07-26 10:58:59 +01:00
Paul Hammerton
d759cc4e94 Merge pull request #8210 from Card-Forge/fix-cardrules-paths
Assign paths to CardRules so we can report where issues come from
2025-07-26 10:58:38 +01:00
Paul Hammerton
4c941df647 Friendly Neighborhood 2025-07-26 10:56:19 +01:00
Paul Hammerton
1f39ad4e91 Edition updates: SPM 2025-07-26 10:47:44 +01:00
Fulgur14
194e1d3356 Friendly Neighborhood (SPM) and its token (#8214) 2025-07-26 08:11:56 +00:00
Chris H
0aa8c933d8 Assign paths to CardRules so we can report where issues come from 2025-07-25 21:56:08 -04:00
Chris H
cf11802fbe Revert "Some handling for the 4 Arena cards that are no longer associated to …"
This reverts commit 7ccb925d76.
2025-07-25 21:25:22 -04:00
Chris H
2703cbc4c9 Restore revision versions 2025-07-25 19:54:42 -04:00
GitHub Actions
b87f3bd00b [maven-release-plugin] prepare for next development iteration 2025-07-25 19:54:42 -04:00
GitHub Actions
a1227167ea [maven-release-plugin] prepare release forge-2.0.05 2025-07-25 19:54:42 -04:00
Chris H
7ccb925d76 Some handling for the 4 Arena cards that are no longer associated to an edition file (#8207) 2025-07-25 19:10:12 -04:00
kevlahnota
beedd17cfe Update SFilterUtil.java
crash fix for Android
2025-07-26 07:07:55 +08:00
kevlahnota
332fa94dab Merge pull request #8065 from Eradev/BetterImageFetcherError
Better image fetcher error
2025-07-26 06:57:40 +08:00
kevlahnota
ae4b5be29f Merge pull request #8008 from Eradev/7859-DisplayTextTooltip
Allow the display of the placeholder in shops and reward windows.
2025-07-26 06:57:27 +08:00
Paul Hammerton
cc4a417c0a Merge pull request #8204 from paulsnoops/edition-updates
Edition updates: PF25, PH23, PLG24, PW25
2025-07-25 19:12:25 +01:00
Paul Hammerton
8a6e03e172 Edition updates: PF25, PH23, PLG24, PW25 2025-07-25 19:09:57 +01:00
Chris H
3ff0c740c4 Update orbital_plunge.txt (#8203) 2025-07-25 18:07:08 +00:00
Eradev
82f8a8f30b Merge remote-tracking branch 'origin/master' into BetterImageFetcherError 2025-07-25 12:37:44 -04:00
kevlahnota
2f7c315822 Merge pull request #8201 from kevlahnota/master2
try to fix dispose to clear more resource
2025-07-25 22:46:33 +08:00
Anthony Calosa
89b9c050a8 try to fix dispose to clear more resource 2025-07-25 22:41:31 +08:00
tool4ever
eb70e21f3d Update merieke_ri_berit.txt 2025-07-25 14:11:59 +00:00
Matthew Scott Krafczyk
ab2b06500b Better Games In Match Selection (#8098)
* Add GamesInMatch combo box selection to booster draft page

Also updated combo box default to be seeded with the stored preferences

* Working comboboxes for desktop version

* Working linked buttons on Mobile

* Add binder classes for preferences and other Model components

* Move to pref binders for mobile GUI
2025-07-25 14:07:19 +00:00
Hans Mackowiak
79845eff1d Update mystic_decree.txt
update Islandwalk to Landwalk:Island
2025-07-25 14:24:24 +02:00
Eradev
9d1b935643 Advanced filtering using the search box (#8077)
* Allow search by type and cmc

* Allow colon and added aliases to cmc

* Support negate

* Extract parser function into its own file

* Support for "!"

* Colors test

* Color search

* Oracle text

* Numeric p/t search

* Set search + fixes

* Typo

* Rarity check

* Loyalty

* Cleanup

* Case ignore for kw

* Support for "or"

* Rename method

* Support parentheses

* Add cases for l

* Cleanup

* Use PaperCardPredicates.printedInSet

* Fix in set to remove conjured cards

* Use func

* Remove redundant check
2025-07-25 11:06:40 +03:00
Fulgur14
ad0c81e984 Update alchemax_slayer_bots.txt (#8198)
* Update alchemax_slayer_bots.txt

* Update TypeLists.txt
2025-07-25 09:25:14 +02:00
Renato Filipe Vidal Santos
e86f4e80cb Spectacular Spider-Man: fixing typos 2025-07-25 05:06:08 +00:00
tool4ever
af4a6de809 Refactor Offering (#8199) 2025-07-25 05:02:57 +00:00
tool4ever
0d0fdd3c0b Update samis_curiosity.txt 2025-07-25 04:05:30 +00:00
Renato Filipe Vidal Santos
de4dcb19cf Quick cleanup: 2025-07-24 2025-07-25 03:54:46 +00:00
kevlahnota
cfcbbb5c21 Update README.md 2025-07-25 09:56:51 +08:00
kevlahnota
6d11361725 Merge pull request #8197 from kevlahnota/master2
refactor planechase BG fetcher
2025-07-25 09:18:02 +08:00
Anthony Calosa
924d9f57a6 minor cleanup 2025-07-25 09:10:32 +08:00
Anthony Calosa
8c5dfb61e7 refactor planechase fetcher 2025-07-25 08:44:21 +08:00
Renato Filipe Vidal Santos
895c5c1e65 SPM: 4 cards (#8194) 2025-07-24 19:14:47 +00:00
tool4ever
e64faa3cf2 Update patchwork_crawler.txt 2025-07-24 18:53:33 +00:00
Paul Hammerton
11815a4cf2 Merge pull request #8195 from paulsnoops/edition-updates
Edition updates: PURL, SPM
2025-07-24 18:51:37 +01:00
Paul Hammerton
d903e4d80d Edition updates: PURL, SPM 2025-07-24 18:47:54 +01:00
kevlahnota
6530515e98 Merge pull request #8193 from kevlahnota/master2
fix planechase bg download
2025-07-25 00:47:20 +08:00
kevlahnota
28711aa669 Merge pull request #8191 from Card-Forge/mkm-crash-fix
Update Murders at Karlov Manor.txt
2025-07-25 00:42:55 +08:00
Anthony Calosa
f3b2192dd2 fix planechase bg download 2025-07-25 00:36:42 +08:00
kevlahnota
d6451ae486 Update Murders at Karlov Manor.txt
fix crash sealed
2025-07-24 23:06:41 +08:00
tool4ever
a78c648e77 Fix Offering being an AltCost (#8190) 2025-07-24 14:33:51 +00:00
Fulgur14
b1afd28556 20 SPM/SPE cards plus Katara (July 24th, Batch 5) (#8184) 2025-07-24 12:45:53 +00:00
Fulgur14
82f5e17705 10 SPM/SPE cards (July 24th, Batch 4) (#8182) 2025-07-24 12:43:27 +00:00
Fulgur14
15e955576f 10 SPM/SPE cards (July 24th, Batch 1) (#8179) 2025-07-24 12:42:08 +00:00
Fulgur14
23b0fa09d9 10 SPM/SPE cards (July 24th, Batch 3) (#8181) 2025-07-24 12:36:18 +00:00
Fulgur14
34e31e7e29 11 SPM/SPE cards (July 24th, Batch 2) (#8180) 2025-07-24 12:34:54 +00:00
kevlahnota
3fcb9e7a3c add stars.png 2025-07-24 19:57:11 +08:00
Anthony Calosa
f69ff10b7c add nyx effect to desktop card renders 2025-07-24 19:31:26 +08:00
tool4ever
bd6ea12cc6 Update coercive_recruiter.txt 2025-07-24 10:10:47 +00:00
Fulgur14
10fa3f7f22 Removing RemoveIntrinsicAbilities$ (#8188) 2025-07-24 11:38:41 +02:00
kevlahnota
462b183548 Merge pull request #8187 from kevlahnota/master2
invert PT Box color
2025-07-24 16:13:21 +08:00
Paul Hammerton
4b5bf6fef4 Update Edge of Eternities.txt 2025-07-24 09:12:08 +01:00
Anthony Calosa
1f28e46e06 invert PT Box color 2025-07-24 15:58:03 +08:00
Paul Hammerton
990e515cb9 Merge pull request #8183 from paulsnoops/paulsnoops-patch-1
Edition updates: SLD, SPE, SPM
2025-07-24 08:26:37 +01:00
Paul Hammerton
6ee86f9762 Update Secret Lair Drop Series.txt 2025-07-24 08:25:02 +01:00
Paul Hammerton
41dfb3488c Create Marvel's Spider-Man.txt 2025-07-24 08:21:45 +01:00
Paul Hammerton
f61e1cd435 Edition updates: SPE, SPM 2025-07-24 08:17:29 +01:00
Fulgur14
1627503248 Origin of Spider-Man (SPM) (#8176) 2025-07-24 07:31:04 +02:00
kevlahnota
4c87c8a1ff update renderer for Spacecraft PT (#8178)
* update renderer for Spacecraft PT
2025-07-24 12:20:00 +08:00
kevlahnota
8036d4a553 Merge pull request #8175 from tool4ever/scpt
Basic renderer support for Spacecraft PT
2025-07-24 07:39:10 +08:00
kevlahnota
a9f18bbf48 Merge pull request #8177 from kevlahnota/newMaster
remove isNyx check
2025-07-24 07:35:40 +08:00
Anthony Calosa
64738f58d6 remove isNyx check 2025-07-24 07:33:42 +08:00
tool4ever
f426e9f236 Update plasma_bolt.txt 2025-07-23 21:29:25 +00:00
tool4EvEr
234237b43a Basic renderer support for Spacecraft PT 2025-07-23 19:11:47 +02:00
tool4ever
854d6640d1 Fix PlayEffect skipping some checks (e.g. split second on AI) (#8162) 2025-07-23 16:26:27 +00:00
Hans Mackowiak
c6514e0183 Update CopyPermanentEffect.java 2025-07-23 17:49:23 +02:00
Hans Mackowiak
9164e76f44 CopyPermanentEffect: fix DefinedName TokenKey 2025-07-23 17:49:23 +02:00
Hans Mackowiak
5cf155fb94 CardFactory: fix Squad TokenNames
Can be used for WH40K token

But FalloutToken need a fallback to Copy?
2025-07-23 17:49:06 +02:00
Renato Filipe Vidal Santos
11be569f35 Rename rukarumal_biologist.txt to rukarumel_biologist.txt (#8173) 2025-07-23 17:37:19 +02:00
kevlahnota
00412f6418 Merge pull request #8171 from kevlahnota/newMaster
QOL Groupby CardType Land
2025-07-23 22:13:43 +08:00
Anthony Calosa
c66e4a08e6 QOL Groupby CardType Land 2025-07-23 22:12:26 +08:00
kevlahnota
c22934f4af Merge pull request #8170 from kevlahnota/newMaster
fix Summon: Choco/Mog not importing
2025-07-23 21:27:46 +08:00
Anthony Calosa
2792c57405 fix Summon: Choco/Mog not importing 2025-07-23 21:26:19 +08:00
Eradev
e3637586d6 Revert "Add g_1_1_insect_flying token"
This reverts commit 0d2d01060d.
2025-07-23 06:53:50 +02:00
Hans Mackowiak
5787dfbca5 Update Edge of Eternities Commander.txt
Fix Insect Token
2025-07-23 06:31:31 +02:00
Fulgur14
9b80408bc6 Token wording update (July 2025 Comprehensive Rules) (#8160)
* Token wording update (July 2025 Comprehensive Rules)

* Update c_a_food_sac.txt

* Update c_a_treasure_sac.txt

* Update c_a_gold_sac.txt

* Update c_e_shard_draw.txt

* Update c_a_clue_draw.txt

* Update incubator_c_0_0_a_phyrexian.txt

* Update role_wicked.txt

* Update c_a_map_sac_explore.txt

* Update c_a_junk_sac_exileplay.txt

* Update c_a_blood_draw.txt

* Update elegy_acolyte.txt

* Update systems_override.txt

* Update survey_mechan.txt
2025-07-22 22:42:15 -04:00
Eradev
073f32c1c9 Update CM2 2025-07-22 21:42:36 -04:00
Eradev
6564e76aad Update CMR 2025-07-22 21:42:36 -04:00
Eradev
35563fe834 Update M21 2025-07-22 21:42:36 -04:00
Eradev
9b46581289 Update M20 2025-07-22 21:42:36 -04:00
Eradev
dbde27bc88 Update PM20 2025-07-22 21:42:36 -04:00
Eradev
b37c280bf8 Update M19 2025-07-22 21:42:36 -04:00
Eradev
643fc2106b Update CNS 2025-07-22 21:42:36 -04:00
Eradev
48496cfbbc Update CN2 2025-07-22 21:42:36 -04:00
Eradev
3022129fb5 Update CON 2025-07-22 21:42:36 -04:00
Eradev
5d0a7b5ef9 Update CM1 2025-07-22 21:42:36 -04:00
Eradev
fa1557c775 Update CMD 2025-07-22 21:42:36 -04:00
Eradev
cb47a9c4be Update CMM 2025-07-22 21:42:36 -04:00
Eradev
9e2ab76be3 Update CLB 2025-07-22 21:42:36 -04:00
Eradev
90c8f8e332 Germ token as Phyrexian Germ token has been printed 2025-07-22 21:42:36 -04:00
Eradev
63397dee07 Update CC2 2025-07-22 21:42:36 -04:00
Eradev
a602b1bdf2 Update CMA 2025-07-22 21:42:36 -04:00
Eradev
8653a26d38 Update CM2 2025-07-22 21:42:36 -04:00
Eradev
8ea5ac97a2 Upcate C19 2025-07-22 21:42:36 -04:00
Eradev
d72d36a8b9 Update C18 2025-07-22 21:42:36 -04:00
Eradev
664ce69993 Update C16 2025-07-22 21:42:36 -04:00
Eradev
93bca202bf Update C14 2025-07-22 21:42:36 -04:00
Eradev
72641e000b Update CSP 2025-07-22 21:42:36 -04:00
Eradev
36696f9b1b Update CST 2025-07-22 21:42:36 -04:00
Eradev
83bfe2a524 Update 6ED 2025-07-22 21:42:36 -04:00
Eradev
827a02563b Update CHR 2025-07-22 21:42:36 -04:00
Eradev
f06acf88f9 Update PCSP 2025-07-22 21:42:36 -04:00
Eradev
0f78751ce3 Update CHK 2025-07-22 21:42:36 -04:00
Eradev
c9fbd579b6 Update PCEL 2025-07-22 21:42:36 -04:00
Eradev
4e12f73444 Update BOK 2025-07-22 21:42:36 -04:00
Eradev
22f4a250e4 Update BBD 2025-07-22 21:42:36 -04:00
Eradev
ed6966b570 Update BFZ 2025-07-22 21:42:36 -04:00
Eradev
14c6794ee3 Update APC 2025-07-22 21:42:01 -04:00
Eradev
9494321965 Update E01 2025-07-22 21:42:01 -04:00
Eradev
34d009a458 Update ARN 2025-07-22 21:42:01 -04:00
Eradev
ac5128d133 Update ACR 2025-07-22 21:42:01 -04:00
Eradev
12c487f7b8 Update PAVR 2025-07-22 21:42:01 -04:00
Eradev
285417275d Update AVR 2025-07-22 21:42:01 -04:00
Eradev
f2db114cb6 Update APC 2025-07-22 21:42:01 -04:00
Eradev
87226d9bd9 Update ATH 2025-07-22 21:42:01 -04:00
Eradev
a644a4c7bc Update ATQ 2025-07-22 21:42:01 -04:00
Eradev
462bdf19b9 Update MPS_AKH 2025-07-22 21:42:01 -04:00
Eradev
3d9bb1f437 Update PAKH 2025-07-22 21:42:01 -04:00
Eradev
485d2dbaf8 Update ALL 2025-07-22 21:42:01 -04:00
Eradev
8f8abdbab9 Update YOTJ 2025-07-22 21:42:01 -04:00
Eradev
7ebe74e57c Update PARB 2025-07-22 21:42:01 -04:00
Eradev
b45153bee8 Update DFT 2025-07-22 21:42:01 -04:00
Eradev
55c4d4240f Update AER 2025-07-22 21:42:01 -04:00
Eradev
0d2d01060d Add g_1_1_insect_flying token 2025-07-22 21:41:36 -04:00
kevlahnota
f84237f25b Merge pull request #8163 from Drecon84/master
Re-adding item to New Game+ Adventure
2025-07-23 06:38:09 +08:00
kevlahnota
07cff6f9a9 Merge pull request #8164 from kevlahnota/newMaster
update SettingsScene landscape checkbox
2025-07-23 06:35:49 +08:00
Anthony Calosa
9f7852e76f update SettingsScene landscape checkbox 2025-07-23 06:29:58 +08:00
Drecon84
943d6edabd Re-adding item to New Game+ Adventure
When I put in the code to delete quest items on a new game+ I forgot to re-add the colorless rune in case you start without the main quest (you get it as part of the main quest). This fixes that.
2025-07-22 21:06:58 +02:00
Matthew Krafczyk
800b88911a Fix draft slot definitions 2025-07-22 14:07:03 -04:00
Matthew Krafczyk
dccbd9912d Small mistakes correction 2025-07-22 14:07:03 -04:00
Matthew Krafczyk
b5f696756e Fix EOS set name 2025-07-22 14:07:03 -04:00
Matthew Krafczyk
9ff528029a Fix draft ranking set codes 2025-07-22 14:07:03 -04:00
Matthew Krafczyk
44046b475b Add EOE to blocks 2025-07-22 14:07:03 -04:00
Matthew Krafczyk
65959f9089 Draft definitions for EOE 2025-07-22 14:07:03 -04:00
Paul Hammerton
94d87039e3 Merge pull request #8161 from paulsnoops/update-draft-ranking
Update draft rankings: DFT, FIN, PIO, TDM
2025-07-22 18:23:49 +01:00
Paul Hammerton
d3a13d4515 Update draft rankings 2025-07-22 18:14:19 +01:00
kevlahnota
b63bee045f Merge pull request #8157 from kevlahnota/newMaster
Prevents crashes when calculating soft buttons bar height
2025-07-22 19:21:24 +08:00
Anthony Calosa
977d46bca5 Prevents crashes when calculating soft buttons bar height 2025-07-22 19:17:57 +08:00
Hans Mackowiak
8293d4de63 Update Edge of Eternities.txt
Add Emblem
2025-07-22 10:41:06 +02:00
Renato Filipe Vidal Santos
94feb1b63b Add files via upload 2025-07-22 09:37:14 +02:00
Renato Filipe Vidal Santos
e37dac6cbe Update giott_king_of_the_dwarves.txt 2025-07-22 09:36:51 +02:00
tool4ever
c7b98bc578 Restore some fixes (#8154) 2025-07-22 06:18:36 +00:00
tool4ever
7d4369fdca Cast after warping only for owner (#8148)
* Cast after warping only for owner

* give effect to owner

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-22 08:23:57 +03:00
kevlahnota
28cb9f9d7d Merge pull request #8152 from kevlahnota/newMaster
fix CardRules setColorID reset
2025-07-22 11:21:02 +08:00
Anthony Calosa
94e3c71809 fix CardRules setColorID reset 2025-07-22 11:19:26 +08:00
Chris H
5dd6a5b45a Revert "Consolidate some PlayEffect logic (#8133)"
This reverts commit 514d40aefd.
2025-07-21 22:03:17 -04:00
kevlahnota
e486f846d8 Merge pull request #8150 from kevlahnota/newMaster
update some dialog translation, KeyBoardDialog
2025-07-22 09:27:00 +08:00
kevlahnota
e7793764a6 Merge pull request #8146 from Drecon84/master
Fix for red castle
2025-07-22 09:01:43 +08:00
Anthony Calosa
5aea2e5bbe update some dialog translation, KeyBoardDialog 2025-07-22 08:58:12 +08:00
Paul Hammerton
f52410f2f8 Merge pull request #8149 from paulsnoops/eoe-historic-bans
EOE Historic bans
2025-07-21 22:00:06 +01:00
Paul Hammerton
53e2f199cc EOE Historic bans 2025-07-21 21:55:48 +01:00
Drecon84
f86e9a56d1 Merge branch 'Card-Forge:master' into master 2025-07-21 20:26:34 +02:00
tool4ever
31ebe48bf3 Update diplomatic_relations.txt 2025-07-21 17:15:09 +02:00
Drecon84
ff963b2cb9 Fix for red castle
Hard mode didn't have the defeat dialogue because it spawned a different version of Lathliss without the defeat code.
2025-07-21 16:45:41 +02:00
kevlahnota
9a6ebce85a Increase MemoryStack
should prevent Out of stack space when exporting the large inventory of cards to clipboard, though not tested for the whole card database
2025-07-21 22:22:48 +08:00
kevlahnota
eb5e37e3de Merge pull request #8145 from kevlahnota/newMaster
fix Rewarded Deck dialogs
2025-07-21 18:42:22 +08:00
Anthony Calosa
92b96aea75 fix Rewarded Deck dialogs 2025-07-21 18:37:07 +08:00
kevlahnota
0f4c0488d5 Merge pull request #8144 from kevlahnota/newMaster
adjust playersprite spawn point on new game
2025-07-21 17:17:10 +08:00
Anthony Calosa
6879089149 adjust playersprite spawn point on new game 2025-07-21 17:10:05 +08:00
tool4ever
3e2eb86bd4 Update dockworker_drone.txt 2025-07-21 07:53:47 +02:00
tool4ever
84ff75c9cf Update frontline_war_rager.txt 2025-07-21 07:52:30 +02:00
tool4ever
9d3605ee96 RepeatEach refactoring (#8139)
* RepeatEach cleanup

* RepeatEach cleanup

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-21 07:22:40 +03:00
Chris H
83cd6b7291 Update wheel_of_sun_and_moon.txt 2025-07-21 06:18:20 +02:00
tool4ever
514d40aefd Consolidate some PlayEffect logic (#8133)
* Fix potential GUI lock if game ends during RepeatEffect

* Fix casting Feast of Blood from PlayEffect without creatures in play
2025-07-20 10:35:03 +00:00
Northmoc
31cca02d8d Flavor for Equip 2025-07-20 09:30:00 +02:00
Hans Mackowiak
5fbd2f9552 Devour: better keyword 2025-07-20 07:45:35 +02:00
Eradev
7ee0431a2f Fix Territorial Bruntar 2025-07-20 06:41:11 +02:00
Eradev
0786403fff Support code2 and alias in sheets (#8009)
* LF

* Support for Code2 and Aliases in sheets

* Support for Code2 and Aliases in sheets

* TryConvertingName

* Revert changes

* Update CardEdition.java

* Wrong check
2025-07-20 07:37:04 +03:00
Renato Filipe Vidal Santos
4f7dbfbc80 Add files via upload (#8004) 2025-07-20 07:36:45 +03:00
Eradev
6caf02fc77 Update Arena sets (#8000)
* Update ANB

* Add XANA

* Update ANA

* Add OANA

* Update G18
2025-07-20 07:36:17 +03:00
Chris Dietsch
9dedc9ecfa Update ISSUES.txt (#8129) 2025-07-20 07:35:23 +03:00
lemtom
b62434616a New 16x16 sprites and avatars for planeswalkers &c. (#8048)
* New sprites that fit established aesthetic

* Adjustments

* Remove downscale + fix wrong pixel on Sorin

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-07-20 07:35:14 +03:00
Eradev
ad8f3ea06f Fix Lobelia 2025-07-19 22:09:08 -04:00
Renato Filipe Vidal Santos
dbed505282 Quick cleanup: 2025-07-19, pass 2 2025-07-19 20:18:24 +00:00
Renato Filipe Vidal Santos
1468746045 Quick cleanup: 2025-07-18, pass 3 (again) (#8124) 2025-07-19 14:44:09 +00:00
Paul Hammerton
6841b31849 Merge pull request #8125 from paulsnoops/eoe-sections
EOE Edition Sections
2025-07-19 15:19:37 +01:00
Paul Hammerton
a151dff3c5 EOE Edition Sections 2025-07-19 15:15:48 +01:00
Paul Hammerton
90f6e2b17f Migrate EOE card scripts 2025-07-19 14:28:57 +02:00
Renato Filipe Vidal Santos
7b1e96410f Add files via upload (#8119) 2025-07-19 11:29:28 +00:00
Paul Hammerton
7ecf619b94 Merge pull request #8122 from paulsnoops/decode-transmissions-name
Fix Decode Transmissions name
2025-07-19 12:10:23 +01:00
Paul Hammerton
b1d8b6fd5c Fix Decode Transmissions name 2025-07-19 12:08:15 +01:00
Paul Hammerton
a15a990730 Merge pull request #8121 from paulsnoops/eoe-formats
EOE Formats update
2025-07-19 12:01:51 +01:00
Paul Hammerton
7402aab2a8 EOE Formats update 2025-07-19 11:53:16 +01:00
Paul Hammerton
2a68427c82 Merge pull request #8120 from paulsnoops/edition-updates
Edition updates: EOC, EOE, EOS, SCH
2025-07-19 11:24:04 +01:00
Paul Hammerton
14dd9721e3 SCH 2025-07-19 11:20:46 +01:00
Paul Hammerton
6115eaf6da EOE 2025-07-19 11:11:42 +01:00
Paul Hammerton
b4922ce353 EOS 2025-07-19 11:06:39 +01:00
Paul Hammerton
0fb37bf2d4 DRAFT: Edition updates 2025-07-19 11:02:20 +01:00
Eradev
8ce5272e95 More console commands (#8096) 2025-07-19 08:33:43 +00:00
Fulgur14
08135545da Final EOE batch (part 4) (#8112) 2025-07-19 07:57:37 +00:00
Fulgur14
3cdbb8f1dd Final EOE batch (part 3) (#8106) 2025-07-19 07:57:24 +00:00
Fulgur14
4f662a5651 Final EOE batch (part 2) (#8105) 2025-07-19 07:56:02 +00:00
Fulgur14
0e0811a1ac Final EOE batch (part 1) (#8103) 2025-07-19 07:51:55 +00:00
Renato Filipe Vidal Santos
71882fe83c EOE: Memorial Vault (#8108) 2025-07-18 20:32:30 +00:00
Northmoc
fce67b847c moxite_refinery.txt and support (#7997) 2025-07-18 14:48:48 +02:00
Renato Filipe Vidal Santos
2ff0c6b92d Add files via upload (#8102) 2025-07-18 12:10:30 +02:00
Paul Hammerton
b27b8b7ba5 Merge pull request #8101 from paulsnoops/master
Edition updates: EOE, EOS, SLD
2025-07-18 10:16:14 +01:00
Paul Hammerton
f889abd57a Update Secret Lair Drop Series.txt 2025-07-18 10:11:28 +01:00
Paul Hammerton
1d2852fff6 Update Edge of Eternities Stellar Sights.txt 2025-07-18 10:03:29 +01:00
Paul Hammerton
48247f7d3b Edition updates 2025-07-18 09:58:56 +01:00
Eradev
d3e6932a23 Fix Dionus' power (#8100) 2025-07-18 10:40:10 +02:00
Renato Filipe Vidal Santos
e4d3e061d1 Quick cleanup: 2025-07-18 (#8099) 2025-07-18 09:32:27 +02:00
Eradev
e57708543a Back to quest list button fix (#8094) 2025-07-18 09:32:11 +02:00
Renato Filipe Vidal Santos
69e2f90c8d EOE: 2 cards (#8097) 2025-07-18 05:49:37 +00:00
shenshinoman
655d01114c Cleaning up Innistrad structure (#8058)
* Cleaning up the file structure to move all Innistrad maps under the Innistrad folder, as well as some minor logic change to Shandalar Old Border's config.json to make maintenance easier. (By setting the desired sets in allowedEditions, we no longer have to restrict every new edition as it comes in.)

Finally, added block info for Alchemy: Innistrad, so it should start being available in events as well.

* Cleaning up the file structure to move all Innistrad maps under the Innistrad folder, as well as some minor logic change to Shandalar Old Border's config.json to make maintenance easier. (By setting all the desired sets to be viewed in allowedEditions, it will only shows those, and we no longer have to restrict every new edition as it comes in.) Also fixed a bug making the "Ghost Town" not work in Shandalar Old Border

Finally, added block info for Alchemy: Innistrad, so it should start being available in events as well.

* Update Adventure - Guardian Gladiolus.dck

---------

Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-07-17 17:29:54 -04:00
Eradev
0d40508e69 Remove conjured cards from rewards 2025-07-17 17:12:11 -04:00
Fulgur14
b785a84dca Mm'menon, Uthros Exile and Pull Through the Weft (EOE) (#8093) 2025-07-17 19:16:32 +00:00
tool4ever
14af90cd27 Henzie day: fix copied spell gaining Blitz (#8092) 2025-07-17 18:57:07 +00:00
Fulgur14
0278918886 Blade of the Swarm (EOE) (#8090) 2025-07-17 17:20:27 +00:00
Fulgur14
b44738cda3 5 EOE cards (July 17th) (#8089) 2025-07-17 16:23:48 +00:00
Fulgur14
ddbe2d6051 3 EOE cards (July 17th) (#8087) 2025-07-17 16:22:30 +00:00
loud1990
a15ebe86db Cleaned up mechan_assembler and weftstalker_ardent triggerzones (#8088) 2025-07-17 16:32:05 +02:00
kevlahnota
16225d2894 Merge pull request #8086 from kevlahnota/newMaster
fix enter key and textfield keypress collision
2025-07-17 20:03:17 +08:00
Anthony Calosa
b4c87a198a fix enter key and textfield keypress collision 2025-07-17 20:02:06 +08:00
Eradev
38da204607 Try to use current set when creating a card from an effect (#8080)
* Fix CardDb fallback
2025-07-17 11:20:15 +00:00
kevlahnota
dbe410f8af Merge pull request #8084 from kevlahnota/newMaster
fix GamePad mapping
2025-07-17 19:18:49 +08:00
Anthony Calosa
b05eb4eed3 fix GamePad mapping 2025-07-17 19:17:36 +08:00
kevlahnota
02201c4527 Merge pull request #8083 from kevlahnota/newMaster
update keybinding for NewGameScene
2025-07-17 19:03:30 +08:00
Anthony Calosa
6536d39969 update keybinding for NewGameScene 2025-07-17 18:59:59 +08:00
Paul Hammerton
c9b41c2b81 Merge pull request #8082 from paulsnoops/paulsnoops-patch-1
Edition updates: EOE, EOS, SPG
2025-07-17 08:38:22 +01:00
Paul Hammerton
74e587a8a0 Update Special Guests.txt 2025-07-17 08:35:13 +01:00
Paul Hammerton
a751ad186e Update Edge of Eternities Stellar Sights.txt 2025-07-17 08:33:26 +01:00
Paul Hammerton
bae41478d9 Edition updates 2025-07-17 08:27:08 +01:00
Renato Filipe Vidal Santos
10dbafbe8c EOE: 4 cards (#8079) 2025-07-16 19:54:02 +00:00
Fulgur14
504946f72a Pinnacle Kill-Ship (EOE) (#8078) 2025-07-16 19:29:48 +00:00
Valensior
e45ae205ac Dauntless Scrapbot [EOE] (#8076) 2025-07-16 18:30:05 +00:00
Fulgur14
4a5969157d 6 EOE cards (July 16th) (#8075) 2025-07-16 17:55:38 +00:00
Fulgur14
bdda4d50f7 Create steelswarm_operator.txt (#8074) 2025-07-16 14:16:42 +00:00
tool4ever
f565943d8d Fix cost syntax (#8073) 2025-07-16 13:02:47 +00:00
Fulgur14
3158c0b1d9 Hymn of the Faller (EOE) (#8072) 2025-07-16 12:57:18 +00:00
Fulgur14
4346d83582 Update atmospheric_greenhouse.txt (#8071) 2025-07-16 06:58:57 +00:00
Fulgur14
7e016f4ca5 6 EOE cards (July 15th) (#8067) 2025-07-16 06:25:09 +00:00
Valensior
38242d0226 [EOE] Atmospheric Greenhouse & Cryogen Relic (#8064) 2025-07-16 07:58:11 +02:00
Eradev
b565095136 Stop spamming key is malformed. 2025-07-15 15:14:06 -04:00
Eradev
93b5fd5f3e Early stop if file exists. Display missing file location. 2025-07-15 15:07:37 -04:00
Eradev
8ca03c1f50 Set output as error 2025-07-15 15:06:36 -04:00
Hans Mackowiak
f5204b89fb lf 2025-07-15 19:53:49 +02:00
Fulgur14
b9dc63b3df Devastating Onslaught (EOE) (#8062) 2025-07-15 17:29:35 +00:00
Valensior
86910302dc Update famished_worldsire.txt (#8063) 2025-07-15 17:20:32 +00:00
Fulgur14
a0cca65075 3 EOE cards (July 15th) (#8060) 2025-07-15 16:56:40 +00:00
Fulgur14
0cc2c3b275 World Shaper EOC Commander Deck (#8011) 2025-07-15 17:22:43 +02:00
Eradev
2cb4856dc7 Daily EOE Update (2025-07-14) (#8006) 2025-07-15 17:22:16 +02:00
Fulgur14
88a53226a2 Create station_monitor.txt (#8057) 2025-07-15 16:49:09 +02:00
Fulgur14
1d42ab50c7 Desculpting Blast (EOE) (#8055) 2025-07-15 15:09:33 +02:00
Fulgur14
cf10851c78 Create meltstrider_eulogist.txt (#8054) 2025-07-15 14:56:05 +02:00
Fulgur14
c1727ffb3f Quantum Riddler (EOE) (#8047) 2025-07-15 13:58:55 +02:00
Fulgur14
43d79c024e Warp/Void EOE scripts, part 3 (#8041) 2025-07-15 12:14:50 +02:00
Fulgur14
3fcf200a86 Warp/Void card scripts, part 4 (#8045) 2025-07-15 11:40:25 +02:00
Fulgur14
8bc5588342 Warp/Void EOE scripts, part 2 (#8039) 2025-07-15 10:28:27 +02:00
Fulgur14
ab27b613e3 Warp/Void EOE scripts, Part 1 (#8038) 2025-07-15 10:25:33 +02:00
tool4ever
c6816dbe74 Close Encounter and support (#8040) 2025-07-15 09:25:32 +02:00
Eradev
41e43a8441 Change PHJ to JP1 (#8044) 2025-07-15 09:23:42 +02:00
Eradev
ff91114f39 Merge branch 'master' into 7859-DisplayTextTooltip 2025-07-14 22:00:22 -04:00
Eradev
b3951555a1 Remove tokens 2025-07-14 20:37:12 -04:00
Eradev
f3a68e940e Update tokens 2025-07-14 20:37:12 -04:00
Eradev
1d646753f2 J21 Update 2025-07-14 20:37:12 -04:00
tool4ever
b293700b13 Fix more MustBlockAi trigger misses (#8037) 2025-07-14 19:03:47 +00:00
Hans Mackowiak
ea2e0fa3e6 Keyword: Warp (#7914)
* Keyword: Warp

* Add Timeline Culler

* Player: remove revolt variable

* AbilityUtils: add Void Modifier

* Improve logic

* Fix import

---------

Co-authored-by: TRT <>
2025-07-14 21:03:36 +02:00
Eradev
bad55a173d Planar Conquest fixes (#8033)
* Only add commanders once. Fix REPRINT_SET_TYPES check.

* Cards.txt example

* Update Rhox Faithmender

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-07-14 19:37:01 +03:00
tool4ever
69538f248d Fix MustBlockAi leaving stuff tapped (#8036) 2025-07-14 16:31:30 +00:00
Agetian
c2f4736f08 AI hint for Olivia's Wrath (#8035)
* - Add puzzles PS_TDM4-5, PS_FIN1-3.

* - Add AI hint for Olivia's Wrath.
2025-07-14 19:28:07 +03:00
Eradev
9aa142ba5a Update S00 (#8030)
Co-authored-by: Agetian <stavdev@mail.ru>
2025-07-14 18:57:48 +03:00
Eradev
f4aba94a18 Fix token generation in puzzles (#8031)
* Fix token generation

---------

Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-07-14 15:23:49 +02:00
kevlahnota
910c06b4d2 Merge pull request #8032 from kevlahnota/newMaster
add restart dialog on World selectbox
2025-07-14 20:44:44 +08:00
Anthony Calosa
490a9b1693 add restart dialog on World selectbox 2025-07-14 20:38:36 +08:00
tool4ever
f6a8705849 Update genestealer_patriarch.txt
Closes #8029
2025-07-14 13:24:42 +02:00
kevlahnota
df2f1105c1 Merge pull request #8023 from kevlahnota/newMaster
update libs for LibGDX 1.13.5
2025-07-14 05:36:38 +08:00
Renato Filipe Vidal Santos
acaf683166 EOE: 2 cards (#8028) 2025-07-13 18:43:19 +00:00
tool4ever
1c6b256268 Fix Spacecraft type collision (#8024) 2025-07-13 18:39:06 +00:00
Agetian
d59ec1527d - Add puzzles PS_TDM4-5, PS_FIN1-3. (#8027) 2025-07-13 20:22:14 +03:00
Anthony Calosa
4b6e46ab7d update libs for LibGDX 1.13.5
update gdx-controllers
2025-07-13 22:27:01 +08:00
Renato Filipe Vidal Santos
68b949bdbf EOE: 3 cards 2025-07-13 14:25:48 +00:00
Fulgur14
a49cc1db97 Update Adventure - Guardian Gladiolus.dck 2025-07-13 10:00:16 -04:00
Simisays
701cc31e1c Update config.json (#8019) 2025-07-13 12:37:22 +03:00
kevlahnota
8832920a4d Merge pull request #8020 from kevlahnota/newMaster
revert TextraTypist
2025-07-13 17:21:07 +08:00
Anthony Calosa
d044d0dc65 revert TextraTypist 2025-07-13 17:12:00 +08:00
Hans Mackowiak
e7427bb180 lf 2025-07-13 10:27:30 +02:00
Fulgur14
989acaa7d4 Lightstall Inquisitor (EOE) (#8018) 2025-07-13 08:19:17 +00:00
tool4ever
6351ea2593 Update lara_croft_tomb_raider.txt 2025-07-13 07:29:29 +00:00
tool4ever
1a586f07d1 Update fraying_sanity.txt 2025-07-13 07:26:42 +00:00
tool4ever
69ec802299 Update ravenous_trap.txt 2025-07-13 07:25:57 +00:00
Hans Mackowiak
817b72f67f lf 2025-07-13 09:10:45 +02:00
Fulgur14
cf1398606b 2 EOE cards (13th July) (#8017) 2025-07-13 07:01:56 +00:00
Fulgur14
31d733aacb Update scouring_swarm.txt (#8014) 2025-07-13 06:11:13 +00:00
kevlahnota
77544f855b Merge pull request #8016 from kevlahnota/newMaster
minor cleanup
2025-07-13 13:44:59 +08:00
Anthony Calosa
1be4880776 fix overlayLabel background 2025-07-13 13:39:53 +08:00
Anthony Calosa
ff39f82099 minor cleanup 2025-07-13 12:52:53 +08:00
shenshinoman
200e509cda Adding extra Planes to Adventure mode and providing an easy method to swap between them. (#7932)
* updating local project

* updating local project

* updating local project

* updating local project

* Update Config.java

updating the file with the better change made by Jetz

* Merge remote-tracking branch 'origin/master'

* Merge remote-tracking branch 'origin/master'

* Merge remote-tracking branch 'origin/master'

* Merge remote-tracking branch 'origin/master'
2025-07-12 21:21:25 -04:00
tool4ever
6a70c6705d Update orims_thunder.txt 2025-07-12 20:52:09 +00:00
kevlahnota
6749e5de7e fix crash (#8013)
* fix illegalargument TextButtonStyles
2025-07-12 23:48:35 +08:00
Eradev
9b83c7eeec Disable the art in the text version 2025-07-12 08:52:15 -04:00
kevlahnota
1c8f7eccbe Update RewardActor (#8012)
* add sell icon

* update RewardActor
2025-07-12 20:03:25 +08:00
kevlahnota
13c98c68e9 update TextraTypist (#8010)
* update TextraTypist

* use pixelmana atlas

* fix variable init
2025-07-12 18:43:03 +08:00
Hans Mackowiak
d492cff553 lf 2025-07-12 10:25:13 +02:00
Fulgur14
875bd54c21 Attempt at Eumidian Wastewaker 2025-07-12 08:13:03 +00:00
Eradev
9825bebb75 Merge branch 'master' into 7859-DisplayTextTooltip 2025-07-11 21:57:48 -04:00
Fulgur14
f2ab574a3b Weftwalking and Monoist Circuit-Feeder (EOE) (#8007) 2025-07-11 19:22:40 +00:00
Eradev
94babc2c53 Fix display 2025-07-11 13:34:13 -04:00
Jetz
4e42963033 Add fantasyBlock support for allowedJumpstart 2025-07-11 12:16:25 -04:00
Jetz
6fec40522e Use code instead of name for filtering drafts 2025-07-11 12:16:25 -04:00
Jetz
eea9890f9d Change allowedJumpstart to override default selection. 2025-07-11 12:16:25 -04:00
Jetz
89d1cffc12 Add allowedJumpstart support to config. 2025-07-11 12:16:25 -04:00
Jetz
40f168bf1e Add restrictedEvents support to config. 2025-07-11 12:16:25 -04:00
Jetz
891a098d2a Per-plane configs 2025-07-11 12:16:25 -04:00
Fulgur14
b6c0d86945 Sami's Curiosity (EOE) (#8005) 2025-07-11 15:29:53 +00:00
Eradev
2cb69c2d6e Allow display of text in tooltip in shops 2025-07-11 11:29:32 -04:00
Eradev
9a96760061 LF 2025-07-11 11:27:12 -04:00
Renato Filipe Vidal Santos
d4a484522e YTDM: 13 more cards (#7486) 2025-07-11 14:59:15 +00:00
Fulgur14
7a24b5cd0c 4 EOE cards (10th July) (#7990)
* Support The Dominion Bracelet
2025-07-11 13:45:04 +00:00
Fulgur14
c0e34799c8 5 EOE cards (11th July) (#8003) 2025-07-11 13:17:22 +00:00
Fulgur14
964d500414 9 EOC + 3 EOE cards (#7991) 2025-07-11 11:19:38 +00:00
Fulgur14
e2949b66fc Create kavaron_harrier.txt (#7999) 2025-07-11 08:34:35 +00:00
Paul Hammerton
06fcb531cd Merge pull request #7993 from Eradev/UpdateEOE20250710
Edge of Eternities Daily Update
2025-07-11 09:15:55 +01:00
Fulgur14
923b12caa0 Hemosymbic Mite (EOE) (#7998) 2025-07-11 08:02:50 +02:00
Chris H
69d75489e7 Fix Bands with other for humans 2025-07-10 22:47:36 -04:00
Eradev
9e985ff471 Edge of Eternities Daily Update 2025-07-10 14:19:52 -04:00
Hans Mackowiak
47390abf3b Update susur_secundi_void_altar.txt
fix Svar SusurDraw
2025-07-10 14:43:04 +02:00
tool4ever
59484f30b5 Fix Keepers (#7988)
* Fix Keepers

* Fix Keepers

---------

Co-authored-by: TRT <>
2025-07-10 15:14:26 +03:00
Eradev
e36701a9f3 Fix Klement, Knowledge Acolyte (#7987) 2025-07-10 12:52:53 +02:00
tool4ever
03b6567993 Fix Seek the Beast for Adventures (again) (#7986) 2025-07-10 11:31:33 +02:00
Eradev
56ef1a1df5 Encode token url (#7983)
* Update 40K

* Apply UTF-8 encoding to tokens too
2025-07-10 10:35:49 +02:00
Fulgur14
6d4af69c19 9 EOC cards (#7982) 2025-07-10 10:31:29 +02:00
Paul Hammerton
b8bd9d92a7 Merge pull request #7985 from Card-Forge/paulsnoops-patch-1
Edition updates: EOC, EOE, EOS, SPG
2025-07-10 09:29:06 +01:00
Paul Hammerton
6a4018f206 oops lands 2025-07-10 09:25:59 +01:00
Paul Hammerton
986b44af43 Update Edge of Eternities.txt 2025-07-10 09:22:46 +01:00
Paul Hammerton
64e6dbad13 ooops space 2025-07-10 09:17:54 +01:00
Paul Hammerton
2e559effcb Update Edge of Eternities Commander.txt 2025-07-10 09:17:15 +01:00
Paul Hammerton
d02f755fa7 Update Edge of Eternities Stellar Sights.txt 2025-07-10 09:14:53 +01:00
Paul Hammerton
17ffda684f Edition updates: EOC, EOE, EOS, SPG 2025-07-10 09:09:35 +01:00
Fulgur14
d6cd2f35ff 2 EOE cards (#7984) 2025-07-10 10:03:45 +02:00
tool4ever
81bba70548 Update monstrous_vortex.txt 2025-07-10 08:49:55 +02:00
Fulgur14
831a2d23e4 7 EOE cards (#7980) 2025-07-10 07:54:13 +02:00
Hans Mackowiak
a0f15cdcb1 lf 2025-07-09 21:04:57 +02:00
tool4ever
e882bd8264 The Eternity Elevator not allowed as Commander (#7978) 2025-07-09 11:45:47 +02:00
Eradev
7101d638c6 Update PRM, TD0, TD1, EOE, EOC (#7976) 2025-07-09 10:58:14 +02:00
Fulgur14
865e3dd783 Update octavia_living_thesis.txt 2025-07-09 10:12:24 +02:00
Fulgur14
b1c992db26 Adagia and Kavaron (EOE) (#7974) 2025-07-09 10:07:42 +02:00
Fulgur14
58f79ae6c0 18 EOE + 2 EOC cards (#7971) 2025-07-09 09:40:02 +02:00
Eradev
fd5af3c93e Fix Meld cards in FIN (#7973) 2025-07-09 05:39:48 +00:00
tool4ever
127d0fced6 Dyadrine, Synthesis Amalgam and support (#7970)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-09 07:28:04 +03:00
Eradev
6526d736bf Prefer random token from set (#7961)
* Prefer random token from set

* Aoivd the array cost

* Forgot import

* ImageUtil

* Fix some tokens
2025-07-09 07:21:09 +08:00
Eradev
cc4a43f0a2 Fix typo (#7955)
* Fix typo

* Support old save files
2025-07-09 07:20:16 +08:00
Fulgur14
286ade792f 12 EOE cards (#7967) 2025-07-08 18:45:18 +00:00
Fulgur14
e681775f47 Update frenzied_baloth.txt (#7969) 2025-07-08 18:38:28 +00:00
Fulgur14
67a9a2c34a 10 EOE cards (#7963) 2025-07-08 18:20:35 +00:00
Eradev
88c0cc00bc Missing emblems + Core sets update (#7966) 2025-07-08 17:51:41 +00:00
kevlahnota
f16f153448 Merge pull request #7968 from Card-Forge/kevlahnota-fix-Rug-of-Smothering
Fix Crash for Rug of Smothering
2025-07-08 22:03:50 +08:00
kevlahnota
1f93dcf5db Update uthros_scanship.txt
- closes #7965
2025-07-08 22:02:44 +08:00
kevlahnota
783fff300d Update AbilityUtils.java 2025-07-08 21:29:53 +08:00
kevlahnota
935e4a00d5 Fix Crash for Rug of Smothering
TriggeredActivator not defined on AbilityUtils calculateAmount, hence produces ClassCastException.
2025-07-08 21:03:39 +08:00
Eradev
6986b5a53d Replace unsupported cards + fix Theros (#7964) 2025-07-08 11:25:20 +02:00
Eradev
e6ed31e9d9 Change Elemental Resonance so it targets CMC>0 (#7962) 2025-07-08 09:26:19 +02:00
Eradev
288c845116 Remove deprecated between to us of (#7951) 2025-07-08 08:39:50 +03:00
Eradev
597222dbf5 Non-alchemy sets cleanup (#7954) 2025-07-08 08:39:17 +03:00
tool4ever
cea6eeabba Update gonti_lord_of_luxury.txt 2025-07-07 15:17:51 +00:00
Eradev
8475456c0b Add missing radiation token (#7948) 2025-07-07 08:51:41 +00:00
Fulgur14
651caccd71 Dawnsire, Sunstar Dreadnought (EOE) (#7958) 2025-07-07 06:18:15 +00:00
kevlahnota
8e9fb8570e Merge pull request #7957 from Eradev/FixEncodingAndroidCompatible
Encode collector number + fallback
2025-07-07 12:55:15 +08:00
Eradev
5a183a6042 Merge branch 'master' into FixEncodingAndroidCompatible 2025-07-07 00:14:02 -04:00
kevlahnota
67e6a7aa1a Merge pull request #7953 from kevlahnota/master2
fix W and S keys for TextField input
2025-07-07 12:09:28 +08:00
Anthony Calosa
b76c67f309 fix comment 2025-07-07 12:08:10 +08:00
Anthony Calosa
99c6b6d815 add comment 2025-07-07 12:06:35 +08:00
Eradev
cadc39699d Encode collector number + fallback 2025-07-07 00:05:37 -04:00
Chris H
8c06aab7b3 Revert "Encode UTF-8 symbols in collector number (#7923)"
This reverts commit fb69f245da.
2025-07-06 22:19:43 -04:00
Anthony Calosa
25c59cd5dd add KeyBinding ispressed condition 2025-07-07 09:37:03 +08:00
Anthony Calosa
9825239e43 fix W and S keys for TextField input 2025-07-07 06:00:04 +08:00
Eradev
fb69f245da Encode UTF-8 symbols in collector number (#7923)
* Fix black star in URL
2025-07-05 22:26:14 +02:00
Robin Woodby
7ef8dddc2a Remove unused import 2025-07-05 14:40:22 -04:00
Robin Woodby
2abcae84b1 Fix issue where card counts from additional printings were ignored 2025-07-05 14:40:22 -04:00
Robin Woodby
b4beb6c182 Remove unnecessary fully qualified names for java.util data structures 2025-07-05 14:40:22 -04:00
Robin Woodby
786d14663b Add copy collection to clipboard button to adventure mode deck editor
Includes translations
2025-07-05 14:40:22 -04:00
tool4ever
f8269e69c4 Fix Rest in Peace making merged cards disappear (#7946)
---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-05 17:50:40 +02:00
tool4ever
29274d0acf StackInstance cleanup (#7947)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-07-05 18:15:55 +03:00
Hans Mackowiak
d6b925f171 move file 2025-07-05 17:01:18 +02:00
Hans Mackowiak
f1f18d1823 lf 2025-07-05 17:00:01 +02:00
tool4ever
03ff2147db True-Name Nemesis crashes on "$" Player name (#7944) 2025-07-05 08:54:14 +00:00
Fulgur14
c66a72806d 6 more leaked EOE cards (#7931) 2025-07-05 08:45:51 +00:00
Fulgur14
e89f2cbac0 Two cards from EOE uncommon leaks that I can actually read (#7895) 2025-07-05 08:34:04 +00:00
tool4ever
d831530c50 Tapestry Warden and support (#7943) 2025-07-05 08:31:38 +00:00
Fulgur14
9fe18c2af8 Evendo and Susur Secundi (#7942) 2025-07-05 08:25:45 +00:00
Fulgur14
fe4ff7ac0f 4 EOE leaks (#7941) 2025-07-05 07:26:04 +00:00
Cees Timmerman
a8ca3d8188 Fix 7937: Offspring token key malformed (#7938) 2025-07-05 07:00:06 +00:00
tool4ever
3b9867c537 Fix updateTarget (#7927)
* Fix meld exiling missing half
2025-07-05 06:58:26 +00:00
Fulgur14
01d22c26f4 Create terrasymbiosis.txt (#7936) 2025-07-04 17:03:28 +00:00
kevlahnota
e8f7fe5a95 Merge pull request #7933 from kevlahnota/master2
fix rounded borders for tokens since it gets the full bordered image …
2025-07-04 23:05:21 +08:00
Anthony Calosa
bd0b8fbc65 minor refactor 2025-07-04 22:45:50 +08:00
Anthony Calosa
05e20b4e92 Merge branch 'master' into master2 2025-07-04 22:39:20 +08:00
Anthony Calosa
716d58ad4b faster implementation 2025-07-04 22:28:51 +08:00
Anthony Calosa
ca12ad529a add comment 2025-07-04 21:58:20 +08:00
Anthony Calosa
6eea82706d better name 2025-07-04 21:52:56 +08:00
Anthony Calosa
d69d005ce0 update remaining checks 2025-07-04 21:27:25 +08:00
Anthony Calosa
b98b322dfe move check 2025-07-04 20:33:57 +08:00
Anthony Calosa
e73e72d150 update view check 2025-07-04 20:10:54 +08:00
Anthony Calosa
8cabc244f7 fix rounded borders for tokens since it gets the full bordered image on scryfall by default now 2025-07-04 19:32:50 +08:00
Fulgur14
4f04e5cc13 Infinite Guideline Station (EOE leak) (#7928) 2025-07-04 10:57:41 +02:00
Agetian
bcf9d2585d - Update LDA data for Standard and Modern (Final Fantasy). (#7926) 2025-07-03 21:42:02 +03:00
Fulgur14
e03b07f940 Create command_bridge.txt (#7924) 2025-07-03 16:45:45 +00:00
Eradev
a45c6aa37e Fix kavu deck (#7919)
* FIx Kavu Deck (C11 -> CMD)

* Whitespaces
2025-07-03 07:00:33 +00:00
Agetian
dd672945f2 - Update LDA data for Standard and Modern (Final Fantasy). (#7918) 2025-07-02 21:25:41 +03:00
Fulgur14
e634be4273 Embrace Oblivion (EOE) 2025-07-02 15:55:41 +00:00
Simisays
9acf5bee41 update (#7916) 2025-07-02 18:25:54 +03:00
tool4ever
1e3297ab64 Update spirit_sisters_call.txt
Closes #7912
2025-07-01 22:11:06 +00:00
Simisays
58df290c8e Adventure church map fix (#7911)
* update

* item name fix
2025-07-01 20:17:40 +03:00
tool4ever
e2075886b1 Update celes_rune_knight.txt 2025-07-01 07:42:42 +02:00
Chris H
ad53abc75a Update snapshot-both-pc-android.yml 2025-06-30 18:38:46 -04:00
Chris H
a599c318dd Update snapshot-both-pc-android.yml 2025-06-30 18:35:51 -04:00
Chris H
14e2a0c5e2 Update snapshot-both-pc-android.yml 2025-06-30 18:28:51 -04:00
Chris H
2ae3efc12e Fix The Brothers War landscape book 2025-06-30 16:48:09 -04:00
tool4ever
fb18134c12 Update merieke_ri_berit.txt
Fix missing trigger when destroyed
2025-06-30 21:13:47 +02:00
Paul Hammerton
2215a101a3 Merge pull request #7906 from paulsnoops/b-a-r-30-june-2025
Banned and Restricted Announcement for June 30, 2025
2025-06-30 18:19:22 +01:00
Paul Hammerton
4df2fe7ac5 Banned and Restricted Announcement for June 30, 2025 2025-06-30 18:08:25 +01:00
Chris H
c24369d1ec Update snapshot-both-pc-android.yml 2025-06-29 12:38:59 -07:00
kevlahnota
41eb86029a Update README.md
Add note to Android installation
2025-06-29 19:52:01 +08:00
Hans Mackowiak
357026ce66 ~ lf 2025-06-28 17:03:26 +02:00
Paul Hammerton
9b558cb069 Merge pull request #7901 from paulsnoops/edition-updates
Edition updates: EOE, EOS, PA1, SLD, TLA & Add Sonic, PA1 to formats
2025-06-28 09:15:11 +01:00
Paul Hammerton
303020ca75 Edition updates: EOE, EOS, PA1, SLD, TLA & Add Sonic, PA1 to formats 2025-06-28 09:10:28 +01:00
Fulgur14
b900abcc71 Sonic the Hedgehog and the rest of his Secret Lair (#7898) 2025-06-28 09:01:20 +02:00
tool4ever
c4ba6df6e9 Update rootwise_survivor.txt 2025-06-28 08:26:08 +02:00
tool4ever
cc4a507799 Update ghouls_night_out.txt
Closes #7900
2025-06-28 08:24:26 +02:00
tool4ever
d8de8ec696 Update professor_hojo.txt 2025-06-27 22:48:18 +02:00
tool4ever
e6563814e8 Amplify: fix only revealing hardcoded types (#7896) 2025-06-27 17:32:52 +02:00
Eradev
016d51669f Better DesecrationDemon sac check. (#7880)
* Better DesecrationDemon sac check.

* Fix var.

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-06-27 06:58:49 +03:00
Jacob Arbib
f19828b2ec Add Duration handling to TextBoxExchangeEffect and added Exchange of Words (#7890) 2025-06-26 17:08:36 +00:00
Hans Mackowiak
36fc87c2a1 ~ lf 2025-06-26 07:03:38 +02:00
tool4ever
4322bddabf Fix NPE by defeating Invasion of Ikoria while controlling Henzie (#7893) 2025-06-25 19:59:27 +00:00
Fulgur14
c128a9d4ba Alpharael, Dreaming Acolyte (EOE) (#7891) 2025-06-25 17:53:25 +00:00
Chris H
2feeffc95c Update jenova_ancient_calamity.txt (#7892) 2025-06-25 17:42:20 +00:00
Hans Mackowiak
53de238a7e Update EnemySprite.java
Fix Android not having stream().toList()
2025-06-25 17:25:46 +02:00
Cees Timmerman
c207e74369 Fix Meld art download (#7858) (#7884)
* Fix Meld art download (#7858)

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-06-25 10:18:43 +02:00
tool4ever
04ca02a77a Update fiery_gambit.txt
Fix collision
Closes #7886
2025-06-22 22:21:28 +02:00
Hans Mackowiak
6ff89c71b6 Add Spacecraft (#7885)
* Add Spacecraft

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-06-22 11:03:52 +02:00
Hans Mackowiak
d7c6a8e53e ~ lf 2025-06-22 10:09:00 +02:00
kevlahnota
150741a443 Merge pull request #7883 from Card-Forge/keybind-hud-android-fix
Fix Android Keybind for Adventure Mode Console toggle
2025-06-22 09:01:36 +08:00
kevlahnota
460f322c44 Update KeyBinding.java
remove duplicates, update back key binding
2025-06-21 22:49:58 +08:00
kevlahnota
ef5d35fe38 Update GameHUD.java
update console ZIndex
2025-06-21 22:46:25 +08:00
kevlahnota
16044556b5 Update Console.java
exit command for console
2025-06-21 22:43:40 +08:00
Paul Hammerton
81d5079468 Merge pull request #7882 from paulsnoops/editionupdates
Edition updates: EOE, PSPL
2025-06-21 11:04:48 +01:00
Paul Hammerton
edc4b22cbc Edition updates: EOE, PSPL 2025-06-21 11:01:04 +01:00
Cees Timmerman
c01dfd4740 Fix Meld art download (#7858) 2025-06-21 11:52:08 +02:00
Paul Hammerton
53a4a46d1f Merge pull request #7881 from paulsnoops/edition-updates
Edition updates: EOC, EOE, EOS, PF25, PJSC, PPRO, SLD, TLA
2025-06-21 10:03:39 +01:00
Paul Hammerton
d61e7e7102 PPRO 2025-06-21 09:59:24 +01:00
Paul Hammerton
17f4df9293 Edition updates: EOC, EOE, EOS, PF25, PJSC, SLD, TLA 2025-06-21 09:55:08 +01:00
Fulgur14
24071582bb 3 EOE cards + 2 EOC cards (#7875) 2025-06-21 07:24:52 +00:00
Eradev
5754466a0d Fix Triplicate Titans tokens ids 2025-06-21 08:27:56 +02:00
Fulgur14
a18eda92b7 Harmonious Grovestrider (EOE) (#7873) 2025-06-20 21:05:18 +00:00
Fulgur14
11d0a8f2fe Create tezzeret_cruel_captain.txt (#7870) 2025-06-20 21:05:10 +00:00
Cees Timmerman
2b8b386882 Fix ConcurrentModificationException (#7865) (#7867)
* Fix ConcurrentModificationException (#7865)

* Update CostPayment.java
2025-06-20 07:33:32 +02:00
Hans Mackowiak
45a5027451 Update kuja_genome_sorcerer_trance_kuja_fate_defied.txt
Closes #7869
2025-06-20 07:32:18 +02:00
Hans Mackowiak
2cb72f55f3 Update Modern Horizons 3.txt
Closes #7864
2025-06-19 10:12:05 +02:00
jpvorenk
fa27fbab46 fix null pointer on draft refresh (#7863) 2025-06-17 21:36:29 -04:00
EfourC
87821fe287 Adventure: Made tooltips instant and reduced size (#7787)
* Made adventure reward tooltips instant and adjusted their size on Desktop

* Moved RewardTooltipManager to be an internal class of RewardActor.
2025-06-16 20:40:08 -04:00
Seth Milliken
cee4bcd867 card: correct sundown pass mana order (#7855)
* card: correct sundown pass mana order
2025-06-15 10:28:23 +02:00
tool4ever
b9cc4c18a8 Update yuna_grand_summoner.txt 2025-06-14 21:56:29 +02:00
EfourC
4577e61940 Corrected height of buttons for disabled Auto and Quck save slots, and adjusted font color. 2025-06-14 13:59:24 -04:00
EfourC
9ac6147a98 Made the autoSell buttons for rewards semi-transparent unless selected and fixed transparency carrying over to other Actors in a batch. Also made small adjustment to card flipping speed to be a little more snappy and fit the sound effect better. 2025-06-14 13:54:14 -04:00
Chris H
74d4ea8a78 Replace each card in a slot individually instead of replacing the whole slot at once. (Fixes higher rates of replacements) (#7854) 2025-06-14 18:02:54 +03:00
Robin Woodby
e2f4c7f872 Autofocus the first dialog option on desktop with keyboard input
Previously, autofocus only worked with gamepads connected.
2025-06-14 10:33:41 -04:00
Robin Woodby
07db8e5a94 Allow ESC to dismiss console in addition to android back button 2025-06-14 10:33:41 -04:00
Robin Woodby
ed7fb03b9c Fix issue where eventTouchUp is firing before keyUp in GameHUD 2025-06-14 10:33:41 -04:00
Robin Woodby
f0ebc3e6a0 Remove redundant call to openMenu causing flickering 2025-06-14 10:33:41 -04:00
Robin Woodby
f2570b4cef Update adventure mode deck editor to support ESC key to go back. 2025-06-14 10:33:41 -04:00
Robin Woodby
97e1939021 Fix enter key not working to save/load in menus 2025-06-14 10:33:41 -04:00
Robin Woodby
bebb894f3e Fix setText call for done button on rewards page 2025-06-14 10:33:41 -04:00
Robin Woodby
288c533c91 Revert reward scene done button text to check mark except for shop
This way translation when loading from file works for "Done"
2025-06-14 10:33:41 -04:00
Robin Woodby
6891f9a5d5 Change back button to esc in shop and reward screens for consistency 2025-06-14 10:33:41 -04:00
Simisays
37a62eed6b Update crypt.tmx (#7853) 2025-06-14 16:40:25 +03:00
MD200210
3ee6b0e58d Details Labels and Zooming on Adventure World Map (#7840)
* Details Button on Adventure Map.
Zooming on Adventure Map

* no Wildcard Import
2025-06-13 07:12:28 +03:00
Robin Woodby
7e6f772345 Update forge-ai description to be more specific and less redundant 2025-06-11 21:05:20 -04:00
Robin Woodby
ef28173b65 Add missing module descriptions in contributing docs 2025-06-11 21:05:20 -04:00
Robin Woodby
a26614c6b6 Remove double spaces and fix typos 2025-06-11 21:05:20 -04:00
Chris H
ee00293e48 Update rydias_return.txt 2025-06-11 16:21:50 -04:00
MatthiasD
ce977387e6 Fix Update Symbols 2025-06-11 16:00:52 -04:00
MD200210
8cc45dbbd8 Fix Cid (#7842) 2025-06-11 17:47:29 +02:00
MatthiasD
851a4c104c Update and change FCA 2025-06-11 16:28:49 +02:00
45ddf3fded Fix: Update getCommander method to enable StorageImmediatelySerialized subfolders 2025-06-11 09:09:10 -04:00
tool4ever
cb538f4026 Update depth_defiler.txt (#7829) 2025-06-09 10:36:34 +02:00
EfourC
6bd6db3c16 Adventure: AccountingLabel animation update 2025-06-08 21:57:07 -04:00
Chris H
6af3940ffb Update queen_brahne.txt 2025-06-08 21:43:34 -04:00
Renato Filipe Vidal Santos
66c864c8d7 Cleanup: DBCleanup (#7822) 2025-06-08 21:32:18 +03:00
rwalters
15354dd8e8 Fix ff limited cards (#7819)
* Fixing the power and toughness of two cards relevant in limited

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-06-08 08:37:56 +03:00
Renato Filipe Vidal Santos
215070fe3f Update blot_out.txt 2025-06-07 12:59:12 +02:00
Renato Filipe Vidal Santos
ed73883e80 Update overtaker.txt (#7815) 2025-06-07 12:59:25 +03:00
Renato Filipe Vidal Santos
df85ebc0aa Update ayula_queen_among_bears.txt (#7812)
* Update ayula_queen_among_bears.txt

* Update rampaging_yao_guai.txt

* Update ulvenwald_bear.txt

* Update rampaging_yao_guai.txt
2025-06-07 10:43:33 +03:00
Hans Mackowiak
885a6af943 Update shambling_cieth.txt
Closes #7810
2025-06-06 17:50:13 +02:00
Renato Filipe Vidal Santos
94820d4782 Update tranquil_frillback.txt 2025-06-06 17:05:11 +02:00
Renato Filipe Vidal Santos
7f044e5ac3 Update wicked_slumber.txt 2025-06-06 15:11:59 +02:00
Renato Filipe Vidal Santos
1284f23620 Update primeval_bounty.txt (#7807) 2025-06-06 12:34:37 +02:00
Agetian
bdcc8acdc4 Add Final Fantasy achievements by Marek14 (#7806)
* - Add FIN achievements by Marek14.

* - Minor tweak.
2025-06-06 10:21:44 +03:00
Chris H
73a9dfcf43 Automate patch version increment 2025-06-05 23:37:40 -04:00
GitHub Actions
7a277ce283 [maven-release-plugin] prepare for next development iteration 2025-06-05 23:37:40 -04:00
GitHub Actions
f8b3f9dd30 [maven-release-plugin] prepare release forge-2.0.04 2025-06-05 23:37:40 -04:00
Chris H
840f3ea96c Automate patch version increment 2025-06-05 22:45:57 -04:00
Chris H
51929434be Update flatten to work with full release 2025-06-05 22:45:57 -04:00
gsonnier333
3ec480afa4 fixed weapons_vendor.txt power/toughness 2025-06-05 21:21:27 -04:00
Chris H
3d19e9e444 Update summon_g_f_ifrit.txt 2025-06-05 21:21:14 -04:00
Renato Filipe Vidal Santos
6d987791f7 Fixing Animate Dead and similar effects) 2025-06-05 14:25:08 +02:00
Renato Filipe Vidal Santos
7b7da00c22 Incidental fixes: 2025-06-05 2025-06-05 08:54:38 +02:00
tool4ever
34791bd892 Update stangg.txt 2025-06-05 08:53:38 +02:00
Renato Filipe Vidal Santos
e6bcd1be72 Add files via upload 2025-06-04 20:33:46 -04:00
Paul Hammerton
4cac354983 Merge pull request #7794 from paulsnoops/fin-formats
Add Final Fantasy to formats
2025-06-04 18:11:13 +01:00
Paul Hammerton
042f7f77a9 Add Final Fantasy to formats 2025-06-04 18:06:21 +01:00
Hans Mackowiak
5fbb1dd0cd Update summon_leviathan.txt
Fix Draw
2025-06-04 08:55:00 +02:00
Hans Mackowiak
bb14dfc00e Update summon_leviathan.txt
Closes #7784
2025-06-04 07:20:24 +02:00
Renato Filipe Vidal Santos
f83cc4ccfa Update fin.rnk 2025-06-03 11:05:10 -04:00
tool4ever
d0d6835a5d Update zodiark_umbral_god.txt 2025-06-03 13:24:17 +00:00
tool4EvEr
104bc8fc55 SpellAbility: set CardState inside constructor 2025-06-03 13:26:25 +02:00
SprinkleMeTimbers
057dd867a8 Adventure: added support for dynamic deck count (#7669)
* Added: utility method for adding an integer selecting combo-box.

* Added: Adventure now supports dynamic amount of decks

* Tweaked: lowered max deck count to 20.

* Added: dynamic deck count can't ever be higher than the maximum in current version.
2025-06-03 13:17:30 +03:00
Chris H
9eed8f5095 Update some names in fin rankings (#7776) 2025-06-03 13:17:18 +03:00
Chris H
1f62be9773 Migrate FIN upcoming (#7777) 2025-06-03 10:13:01 +00:00
Hans Mackowiak
7a46f92059 Update CardState updateSpellAbilities only when not inPlay (#7780)
* Fix copy order
2025-06-03 10:12:35 +00:00
tool4EvEr
cfa79b9676 attachAuraOnIndirectETB: fix missing activator 2025-06-03 09:54:37 +02:00
tool4ever
c0a63fa15b Fix Dark Depths triggering vs. Blood Moon (#7768) 2025-06-03 08:07:10 +02:00
Renato Filipe Vidal Santos
c61537ae16 Add files via upload 2025-06-02 23:44:22 -04:00
Renato Filipe Vidal Santos
3871095b92 Add files via upload 2025-06-02 23:44:22 -04:00
Simisays
dee846da49 Update cave_bigzombie.tmx 2025-06-02 22:08:08 -04:00
Jason Vorenkamp
2477553d13 fix incorrect resource name 2025-06-02 21:46:37 -04:00
Renato Filipe Vidal Santos
c9df7d7f8e Cleaning upcoming: 2025-06-02 2025-06-02 16:02:17 +00:00
rwalters
53cb093f9e (Adventure) Fix draft scene transition (#7666)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fixing a bug in which the transition screen's non-blocking of the start match button can be clicked multiple times, which results in a crash when the match ends
2025-06-02 10:24:50 -04:00
MD200210
28e86970dc Added Draft Info (#7764)
* FIN Draft Info

(cherry picked from commit ad0385617b26391efa2b866157eea674ea850e1e)

* Correction

* Correction

* Correction
2025-06-02 10:24:10 -04:00
Chris H
f847fc1669 Fix sketchbooks that are missing colons in the name 2025-06-02 07:22:26 -04:00
Hans Mackowiak
f3df55177a CardState: fix Android not having stream().toList(); Closes #7761 2025-06-02 07:10:26 +02:00
Chris H
be6f345127 Hotfix PaperCard edition issues 2025-06-01 22:50:58 -04:00
Seth Milliken
7fa89cbc2e fix: do not steal keyboard focus when "Visually Alert on Receipt of Priority" is enabled
When the "Visually Alert on Receipt of Priority" feature is enabled,
the `showTab()` call at the beginning of `forge.gui.framework.SDisplayUtil.remind()`
steals focus from the primary button whenever anything goes on the
stack.

This change, previously suggested by pfps as probably not necessary, is
indeed necessary after all to prevent that focus stealing by restoring
focus to the previous owner only if the owner was an `FButton`.

fixes https://github.com/Card-Forge/forge/issues/7660
2025-06-01 14:17:09 -04:00
Greg Sonnier
567e13c92b fixed locke_treasure_hunter.txt unblockable condition (#7752) 2025-06-01 08:30:27 +00:00
Hans Mackowiak
6f5a933de3 CardState: Refactor LandAbility/Aura/PermSpell (#7680)
* CardState: Refactor LandAbility/Aura/PermSpell

* Update CardState.java

Fix Room

* unify manaAbilities and nonManaAbilities and fix Room cards

* CardView: fix Left&Right Split not update abilities

* AbilityFactory: make Fuse Ability Secondary

* CardFactory: remove extra logic for LeftSplit and RightSplit

* AbilityFactory: set OriginalAbility for Fuse Parts

* SpellPermanent: fix Desc for CardState

* PermanentCreatureEffect: use getBasePowerString/getBaseToughnessString

* LandAbility: fix Modal Lands
2025-06-01 09:53:40 +02:00
kvn1338
e40567c9c8 Add TextBoxExchangeEffect Ability and 'Deadpool, Trading Card' (#7637)
* capure Textboxes to avoid LKI copy

* Fix copying keyworded traits twice

* Support keepTextChanges across all traits

Co-authored-by: kvn <kevni@secure.mailbox.org>
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-06-01 07:19:50 +00:00
tool4ever
928ac875b5 Fix only turning fully unlocked rooms face down (#7742)
Co-authored-by: tool4EvEr <tool4EvEr@>
2025-05-31 13:28:52 +02:00
Paul Hammerton
f4831bc51b Merge pull request #7740 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, SLD
2025-05-31 11:19:28 +01:00
Paul Hammerton
d3e0e79325 Edition updates: FCA, FIC, FIN, SLD 2025-05-31 11:05:48 +01:00
Renato Filipe Vidal Santos
ac3015eff1 Add files via upload (#7730) 2025-05-31 07:52:06 +00:00
tool4ever
554e73e352 Fix Estinien Varlineau (#7738) 2025-05-31 06:42:04 +00:00
Hans Mackowiak
fe86bb1be3 line endings 2025-05-31 00:01:54 +02:00
Fulgur14
f98eca3925 The FINAL Fantasy cards (#7736) 2025-05-30 18:01:51 +00:00
Fulgur14
8506fd2bab 4 FIN cards (#7727) 2025-05-30 18:01:34 +00:00
Hans Mackowiak
c2ffa227e2 Saga: do State-Based and Turn-Based Action only with Chapters (#7737) 2025-05-30 17:16:15 +00:00
Hans Mackowiak
f33f780b25 line endings 2025-05-30 18:58:30 +02:00
Renato Filipe Vidal Santos
855e4dcabd FIN: Memories Returning (#7731) 2025-05-30 10:23:13 +02:00
Fulgur14
40c9a06b21 Couple fixes (#7732)
* Update breaching_hippocamp.txt

* Update skanos_dragon_vassal.txt
2025-05-30 08:52:06 +03:00
Chris H
7c4cf9425f Update INSTALLATION.txt 2025-05-29 10:11:13 -04:00
Fulgur14
d2a6329e03 Leaked Rydia and Exdeath (FIN) (#7725)
* Leaked Rydia and Exdeath (FIN)

* Update exdeath_void_warlock_neo_exdeath_dimensions_end.txt

* Update rydia_summoner_of_mist.txt
2025-05-29 13:33:12 +03:00
Renato Filipe Vidal Santos
6e2717e371 FIN: 11 cards (#7724) 2025-05-29 09:22:54 +00:00
Chris H
2152b7ca7d Fix line endings 2025-05-28 23:25:57 -04:00
Fulgur14
08387f12cb Summon: Brynhildr (FIN) (#7722) 2025-05-28 19:48:22 +00:00
tool4ever
01800d3c49 Update summon_valefor.txt 2025-05-28 19:03:00 +02:00
Fulgur14
2b3735662e 7 FIN cards (#7721) 2025-05-28 18:59:07 +02:00
Renato Filipe Vidal Santos
5ab2b44343 FIN: Golbez, Crystal Collector 2025-05-28 18:48:42 +02:00
Renato Filipe Vidal Santos
0bdb9d7d27 FIN: 7 cards (#7717)
* Add files via upload

* Update thiefs_knife.txt

* Update swallowed_by_leviathan.txt

* Update aether_spike.txt

* Update swallowed_by_leviathan.txt
2025-05-28 15:45:16 +02:00
Renato Filipe Vidal Santos
1f4cebf186 FIN: The Masamune (#7719) 2025-05-28 14:04:50 +02:00
rwalters
f651a7d73d (Adventure Mode) Fix quest references and step requirements (#7697)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fixing several text based references that looked created in bulk with similar problems. Kiora and Teferi don't require going back to the town that issued the quest, and the others reference the wrong location when arriving there for where the reputation goes
2025-05-28 11:45:24 +03:00
Fulgur14
453bae6849 Summon: G.F. Cerberus and X-ATM092 (FIN) (#7715) 2025-05-28 09:47:38 +02:00
Fulgur14
292cca0727 10 FIN cards, batch #3 (#7713) 2025-05-28 05:43:27 +00:00
Fulgur14
24f568101f 10 FIN cards, batch #2 (#7712) 2025-05-28 05:41:34 +00:00
tool4ever
ad73651382 Prishe's Wanderings: support CantSearch case (#7711) 2025-05-28 05:40:38 +00:00
Chris H
476f3dfe9c Fix line endings 2025-05-27 16:07:02 -04:00
Fulgur14
5977129096 10 FIN cards (#7710) 2025-05-27 19:58:55 +00:00
Greg Sonnier
44e243d428 fixed hraesvelgr_of_the_first_brood.txt mana cost (#7708) 2025-05-27 18:21:55 +00:00
Fulgur14
491e275b8a Rufus and Seifer (#7707) 2025-05-27 16:54:42 +00:00
Renato Filipe Vidal Santos
7bf6ba0779 FIN: Minwu (#7706) 2025-05-27 16:30:54 +00:00
Fulgur14
ea33d589ae Diamond Weapon (FIN) (#7702) 2025-05-27 16:30:02 +00:00
Renato Filipe Vidal Santos
1314c98a5a Add files via upload (#7701) 2025-05-27 17:29:23 +03:00
Fulgur14
66c1c485f2 Magic Damper (FIN) (#7704) 2025-05-27 14:28:41 +00:00
Hans Mackowiak
fe1d37bed6 Update Ikoria Lair of Behemoths.txt
Closes #7700
2025-05-27 13:34:38 +02:00
Fulgur14
64d54dc7c7 Gysahl Greens (FIN) (#7699) 2025-05-27 08:24:08 +00:00
Renato Filipe Vidal Santos
050cebf944 FIN: 6 cards (#7696) 2025-05-27 08:22:13 +02:00
Renato Filipe Vidal Santos
42e5d9437b Adventure cleanup: 2025-05-26 (#7695)
* Add files via upload

* Update quests.json

* Update quests.json

* Update shops.json

* Update skep_outer.tmx
2025-05-27 07:20:08 +03:00
Fulgur14
67aaf9902e Cactuar and Light of Judgment (FIN) (#7692) 2025-05-26 20:15:52 +00:00
Fulgur14
3d0436aeca Restoration Magic (FIN) (#7690) 2025-05-26 22:32:28 +03:00
Simisays
827d60610d Update fort_white_4_farm.tmx (#7694) 2025-05-26 22:29:53 +03:00
Renato Filipe Vidal Santos
69a1d76778 FIN: 2 cards 2025-05-26 18:15:15 +00:00
tool4ever
7bd5dbbe28 Fix text change of keywords missing traits (#7689) 2025-05-26 15:41:16 +00:00
Fulgur14
39613a8413 3 FIN commons (#7688) 2025-05-26 14:42:26 +00:00
Renato Filipe Vidal Santos
225ff335e8 FIN: Midgar, City of Mako (#7685) 2025-05-26 14:10:03 +00:00
Paul Hammerton
23a3de5446 Merge pull request #7684 from paulsnoops/edition-updates
Edition updates: FCA, FIN
2025-05-26 11:32:01 +01:00
Paul Hammerton
75fd7bbfda Edition updates: FCA, FIN 2025-05-26 11:27:50 +01:00
tool4ever
ed6a14e180 Fix LKI missing changed text (#7683) 2025-05-26 08:35:57 +00:00
Fulgur14
42b6d2c17d Dion, Bahamut's Dominant (FIN) (#7682) 2025-05-26 08:23:59 +00:00
Hans Mackowiak
9a93f0a16c TokenDb: fix Endure Token Images causing crash on Token Viewer (#7679) 2025-05-25 19:27:13 +02:00
Renato Filipe Vidal Santos
1c0d3031d3 MB2: 2 cards (#7678) 2025-05-25 17:05:22 +00:00
Hans Mackowiak
724697391c Update Aetherdrift Commander.txt 2025-05-25 18:03:42 +02:00
Renato Filipe Vidal Santos
7820c3f519 Adventure cleanup: 2025-05-25 (#7674)
* Update quests.json

* Update shops.json

* Update bluewizard_hard_bounce.dck

* Update kavu_domain.dck

* Update sandghoul.dck

* Update aerie_0.tmx

* Update barbariancamp_kobold_mine.tmx

* Update evilgrove_5_swamp.tmx

* Update grolnok_f1.tmx

* Update grove_1_bears.tmx

* Update black_castle.tmx

* Update blue_castle.tmx

* Update green_castle.tmx

* Update red_castle.tmx

* Update white_castle.tmx

* Update graveyard.tmx

* Update steppe.tmx

* Update town.tmx

* Update bog.tmx

* Update graveyard.tmx

* Update town.tmx

* Update maze_2.tmx

* Update slime_hive.tmx

* Update wastetown..tmx

* Update skep_outer.tmx

* Update tibalt_f1.tmx

* Update tibalt_f2.tmx

* Update tibalt_f3.tmx

* Update main.tsx

* Update enemies.json

* Update skep_outer.tmx

* Update items.json

* Update quests.json

* Update shops.json

* Update town_names_black.txt

* Update aerie_0.tmx

* Update sorins_amulet.txt

* Update sorins_boss_effect.txt
2025-05-25 16:35:28 +03:00
Renato Filipe Vidal Santos
fe80b3850e Update carrionette.txt (#7650) 2025-05-25 16:35:17 +03:00
rwalters
16a2ae6741 (Adventure Mode): Fix abandoned town (#7665)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Adventure Mode: Fix Quest_APortalToNowhere, in which the file path got moved but the point of interest json was not updated

* Manually altering points_of_interest.json instead of using the adventure editor tool

* Actual change was removed during synchronization with remote head, re-applying actual fix
2025-05-25 08:47:23 -04:00
tool4ever
ce19c4fb9d Update capricious_sliver.txt
Closes #7672
2025-05-24 20:21:00 +00:00
tool4ever
4911a7f951 Update professor_hojo.txt 2025-05-24 17:40:36 +00:00
Renato Filipe Vidal Santos
c6f994d47a Quick cleanup: 2025-05-24 2025-05-24 16:20:19 +00:00
Hans Mackowiak
f0a9077791 lf 2025-05-24 16:38:52 +02:00
Fulgur14
b6941a9b38 Gogo, Master of Mimicry (FIN) (#7655) 2025-05-24 13:16:13 +00:00
Fulgur14
7023eb10d3 10 FIN cards (Vaan and his band) (#7661)
* 4 FIN cards (Vaan and his band)

* Add files via upload

* Add files via upload

* Add files via upload
2025-05-24 16:12:46 +03:00
tool4ever
007e132559 UnlessCost: fix checking wrong payer (#7663)
* UnlessCost: fix checking wrong payer

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-05-24 12:10:48 +02:00
tool4ever
9b9fdb2e5e Edgar, King of Figaro and support (#7645) 2025-05-24 07:27:48 +00:00
Renato Filipe Vidal Santos
f1f4310608 FIN: Black Waltz No. 3 2025-05-24 06:36:44 +00:00
Renato Filipe Vidal Santos
8634f96e4b FIN: 3 cards (#7646) 2025-05-23 15:20:26 +00:00
Fulgur14
6eed4c31a2 Raubahn, Bull of Ala Mhigo (FIN) (#7659) 2025-05-23 14:22:31 +00:00
Hans Mackowiak
c7b9934072 ImageFetcher: add downloadUrl for HiddenCard (#7651) 2025-05-23 13:51:00 +02:00
Paul Hammerton
cc50d72d63 Merge pull request #7654 from paulsnoops/edition-updates
Edition updates: FCA, FIN, SLD
2025-05-23 10:10:34 +01:00
Paul Hammerton
fbceab3252 fix-the-gold-saucer 2025-05-23 10:06:11 +01:00
Paul Hammerton
780491ae4b Edition updates: FCA, FIN, SLD 2025-05-23 09:56:26 +01:00
tool4ever
994471e9b6 Update verazol_the_split_current.txt 2025-05-23 10:50:35 +02:00
Chris H
e1bf639e90 Fix broken json in tmx file 2025-05-22 21:43:40 -04:00
Chris H
4386cead3e Fix end lines 2025-05-22 21:43:19 -04:00
Fulgur14
9b0d0f7924 8 FIN Cards (#7648) 2025-05-22 20:53:45 +00:00
Fulgur14
b9be1bc647 Omega, Heartless Evolution (FIN) (#7647) 2025-05-22 17:16:10 +02:00
tool4ever
513e4c6c8d Update hraesvelgr_of_the_first_brood.txt 2025-05-22 16:37:28 +02:00
tool4ever
0f5b67a504 Update lyse_hext.txt 2025-05-22 16:36:52 +02:00
Fulgur14
72f455067f Tellah, Great Sage (FIN) (#7644) 2025-05-22 16:36:13 +02:00
Hans Mackowiak
6dc508b631 ~fix Token ImageKeys without Set (#7643) 2025-05-22 09:29:33 +03:00
Hans Mackowiak
deecf128c3 Fix Henzie again with SvarFallback (#7604)
* Fix Henzie again with SvarFallback

* henzie rename Svar
2025-05-22 07:20:22 +02:00
Renato Filipe Vidal Santos
5f497669e5 Quick cleanup: 2025-05-21 (#7642) 2025-05-21 20:16:00 +00:00
Fulgur14
90952d95c9 Summon: G.F. Ifrit (FIN) (#7641) 2025-05-21 18:49:24 +00:00
Renato Filipe Vidal Santos
5a5ef52492 FIN: Clash of the Eikons 2025-05-21 18:27:22 +00:00
Fulgur14
3b4df7211c Jenova, Snow, and Ether (FIN) (#7638) 2025-05-21 18:25:28 +00:00
Renato Filipe Vidal Santos
45329a6df0 FIN: 5 more cards (#7627) 2025-05-21 18:25:11 +00:00
Fulgur14
22607adf57 Qiqirn Merchant and the 9 remaining Town duals. (#7639) 2025-05-21 18:06:34 +00:00
Agetian
bd9c72fee9 - Update Libgdx to 1.13.5. (#7636) 2025-05-21 18:12:33 +03:00
Renato Filipe Vidal Santos
35641265dd Update remorseless_punishment.txt (#7635) 2025-05-21 17:43:05 +03:00
Renato Filipe Vidal Santos
fe37a8358b Fixing Firion (#7633) 2025-05-21 14:23:56 +00:00
Fulgur14
bb7ae64ef8 Slash of Light (FIN) (#7632) 2025-05-21 09:46:28 +00:00
Fulgur14
4d9db78cc3 Aettir and Priwen (FIN) (#7631) 2025-05-21 06:18:15 +00:00
Agetian
ce46c684b5 - Fix AI logic for Sorin, Vengeful Broodlord. (#7630) 2025-05-21 09:03:54 +03:00
Fulgur14
29d4e716f5 The remaining 4 Crystals and some other FIN cards (#7628) 2025-05-21 05:02:21 +00:00
Chris H
c6bfdd5b78 Landscape Sketchbook expansion (#7618)
* Expand landscape sketchbooks

* Remove the sketchbooks for sale that are earned via quests
2025-05-21 06:57:58 +03:00
Hans Mackowiak
559daf9fce update ImageKey for Other 2025-05-20 22:58:22 -04:00
Hans Mackowiak
9caa024fa5 transformed token image 2025-05-20 22:58:22 -04:00
Hans Mackowiak
3605b4e34e uppercase token set and fallback token 2025-05-20 22:58:22 -04:00
Hans Mackowiak
56832ff987 ~ update set token image location 2025-05-20 22:58:22 -04:00
Hans Mackowiak
338bb09747 PaperToken: cleanup imageKey without collectorNumber 2025-05-20 22:58:22 -04:00
Hans Mackowiak
b57a5e9ad1 PaperToken: add CollectorNumber and update Downloader 2025-05-20 22:58:22 -04:00
Simisays
67f8f4760e Update skep_outer.tmx 2025-05-20 22:51:20 -04:00
Ryan Walters
485427c682 Fix poi references and an exception generated by failing to address non-swapped poi references, focusing on quest 18, A Focused Mind 2025-05-20 09:05:21 -04:00
Renato Filipe Vidal Santos
8852732e6b FIC: 4 cards (#7624) 2025-05-20 12:27:28 +00:00
Renato Filipe Vidal Santos
6264761117 Quick cleanup: 2025-05-20 (#7622) 2025-05-20 11:07:29 +00:00
Paul Hammerton
a3b1c98abf Merge pull request #7620 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, PF25, PPRO, PSS5, PW25, SCH, SLD
2025-05-20 11:10:31 +01:00
Paul Hammerton
c566a4bed8 oops 2025-05-20 10:59:42 +01:00
Paul Hammerton
a16e2a9c37 don't add FSPL because JP only and same art 2025-05-20 10:53:43 +01:00
Paul Hammerton
f80bb13ed7 Edition updates: FCA, FIC, FIN, FSPL, PF25, PPRO, PSS5, PW25, SCH, SLD 2025-05-20 10:48:58 +01:00
Fulgur14
1c350b1766 From Father to Son (FIN) (#7619) 2025-05-20 09:24:36 +00:00
Fulgur14
0b5ce9c8fc Add 4 cards [FIN]
* Add files via upload

* Update TypeLists.txt

* Create g_1_1_frog.txt

* Update quina_qu_gourmet.txt

I realized that I forgot to set the amount of tokens.
2025-05-20 09:52:01 +03:00
Fulgur14
74c7f4b164 Coral Sword (FIN) (#7614) 2025-05-20 09:47:02 +03:00
Fulgur14
6128f1d720 Queen Brahne and Lindblum (FIN) (#7615)
* Add files via upload

* Add files via upload
2025-05-20 09:46:48 +03:00
Fulgur14
9e2dcbb630 Haste Magic (FIN) (#7616) 2025-05-20 09:46:12 +03:00
Hans Mackowiak
ec9fc88734 ~ fix IterableUtil.and and Iterable.or usage (#7607) 2025-05-20 05:47:26 +02:00
Renato Filipe Vidal Santos
59c404f6c4 Cleanup: Blazing Crescendo (#7617) 2025-05-19 19:40:00 +00:00
Renato Filipe Vidal Santos
fd727a909b Update white_auracite.txt (#7613) 2025-05-19 16:59:50 +02:00
Chris H
ce6ad65e12 Add some combat tests for the AI to help improve poor combat areas (#7600)
* Add some basic testing for AI Combat decisions

* Add some basic testing for AI Combat decisions
2025-05-19 12:30:53 +03:00
tool4ever
6b65f8972c Update lupine_prototype.txt 2025-05-19 09:05:03 +02:00
Renato Filipe Vidal Santos
2e7fe8a81b FIN: Ice Flan 2025-05-19 07:35:42 +02:00
tool4ever
6f9db790a6 Fix updating room in exile (#7608)
* Fix wrong zone breaking X
2025-05-18 18:04:48 +00:00
Simisays
645aff52cb Fix Sanguine Soothsayer (#7606) 2025-05-18 11:38:51 +00:00
Hans Mackowiak
4d4afbdf03 lf 2025-05-18 13:01:33 +02:00
Renato Filipe Vidal Santos
a8c1f5c969 Quick cleanup: 2025-05-17 (#7597) 2025-05-18 09:33:29 +00:00
Fulgur14
0f4c94d6f8 PuPu UFO (#7603) 2025-05-18 09:33:06 +00:00
Renato Filipe Vidal Santos
53479c60b4 Fixing Living Lectern, Embereth Veteran (#7599)
* Update living_lectern.txt

* Update embereth_veteran.txt
2025-05-18 09:43:22 +02:00
Chris H
0c7b8c5b04 Collecting Landscape Sketchbooks adds that set to the basic land dialog (#7602)
* Collecting Landscape Sketchbooks adds that set to the basic land dialog

* Optimize the edition mapping
2025-05-18 07:33:11 +03:00
tool4ever
7860957d8f Update astrologians_planisphere.txt 2025-05-17 16:04:24 +00:00
Renato Filipe Vidal Santos
07bc31f4c1 FIN: 2 more cards (#7592) 2025-05-17 12:37:01 +02:00
Paul Hammerton
99390f5967 Merge pull request #7595 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, RFIN, SLD
2025-05-17 11:29:12 +01:00
Paul Hammerton
ee8ca02128 Edition updates: FCA, FIC, FIN, RFIN, SLD 2025-05-17 11:12:04 +01:00
Hans Mackowiak
1bc7efba65 Update Cards with WithMayLook (#7593) 2025-05-17 10:39:20 +02:00
Hans Mackowiak
efc2357905 lf 2025-05-17 09:16:55 +02:00
Fulgur14
d5d01011f4 Update fandaniel_telophoroi_ascian.txt (#7590) 2025-05-17 06:21:34 +00:00
Fulgur14
c1a19ea4ae Buster Sword (FIN) (#7589) 2025-05-17 06:11:40 +00:00
Fulgur14
ad3fb1137a The Falcon, Airship Restored (FIC) (#7586) 2025-05-17 06:10:53 +00:00
Renato Filipe Vidal Santos
c5c6b36f4f FIC: last 2 cards + FIN: 2 cards (#7576)
* Add files via upload

* Add files via upload

* Add files via upload
2025-05-17 07:41:23 +03:00
Fulgur14
f2f92212bc How to suplex a train (#7578)
There's something strange with Phantom Train's ability where its P/T display doesn't update properly after getting the counter -- maybe because the counter is gained before animating?
2025-05-17 07:41:11 +03:00
Fulgur14
3226be58be Serah, Sazh, and Anima (FIN) (#7585)
* Serah, Sazh, and Anima (FIN)

* Update serah_farron_crystallized_serah.txt
2025-05-17 07:41:03 +03:00
Renato Filipe Vidal Santos
46cfcff0ca Update dancers_chakrams.txt 2025-05-16 23:01:48 -04:00
Fulgur14
b161c9612b Triple Triad (FIN) (#7572) 2025-05-16 18:43:59 +00:00
Renato Filipe Vidal Santos
91dee5c379 FIN: another 2 cards 2025-05-16 18:43:47 +00:00
Fulgur14
349129f88f Quistis Trepe (FIN) (#7581) 2025-05-16 15:50:27 +00:00
Hans Mackowiak
57ea25bbbd Update white_auracite.txt
Closes #7579
2025-05-16 15:43:11 +02:00
Hans Mackowiak
d1725af64d Update urianger_augurelt.txt
Close #7575
2025-05-16 13:19:14 +02:00
Fulgur14
72626ef214 Choco, Seeker of Paradise and Paladin's Arms (FIN) (#7571) 2025-05-16 06:25:02 +00:00
Chris H
e00d5ee30b Fix line endings 2025-05-15 22:19:14 -04:00
tool4ever
d6c42c3c8c Update hildibrand_manderville.txt
Closes #7574
2025-05-15 21:06:53 +00:00
Greg Sonnier
950a1f2a44 fixed missing cleanup in graha_tia_scion_reborn.txt (#7573) 2025-05-15 20:35:17 +00:00
Renato Filipe Vidal Santos
c895b0eeab FIC: yet another 5 cards (#7570) 2025-05-15 18:50:48 +00:00
Fulgur14
82c8fb20e8 Jecht and 2 other FIN cards (#7562) 2025-05-15 18:18:20 +02:00
Fulgur14
cf54f3e04e Relm's Sketching (FIN) (#7569) 2025-05-15 17:27:05 +02:00
Fulgur14
f4958a4b49 9 FIC/FIN cards... plus a token (#7561) 2025-05-15 15:51:13 +02:00
Renato Filipe Vidal Santos
760f9412ff FIC: another 5 cards 2025-05-15 15:15:57 +02:00
Fulgur14
71f2d41eb0 Sin, Unending Cataclysm (FIC) (#7563) 2025-05-15 15:15:38 +02:00
Fulgur14
51d7933ef3 10 FIC cards, batch 3 (#7547) 2025-05-15 15:13:45 +02:00
Fulgur14
f5938b47e1 10 FIN/FIC cards, batch 8 2025-05-15 15:13:35 +02:00
tool4ever
807d078799 Support for Lifestream's Blessing (#7565) 2025-05-15 15:11:20 +02:00
Renato Filipe Vidal Santos
bedb97183b FIC: 7 more cards (#7557) 2025-05-15 15:11:04 +02:00
Fulgur14
08919e3375 Summon: Yojimbo and Capital City (#7568) 2025-05-15 14:34:19 +02:00
Fulgur14
a117d65f51 10 FIN/FIC cards, batch 9 (#7560) 2025-05-15 14:19:26 +02:00
Fulgur14
b61e3015f3 8 FIN/FIC cards (#7556) 2025-05-15 13:09:49 +02:00
Fulgur14
83a512a075 10 FIN/FIC cards, batch 5 (#7550) 2025-05-15 13:08:39 +02:00
Fulgur14
c1630a2e47 Rejoin the Fight (FIC) 2025-05-15 13:08:11 +02:00
Fulgur14
1d0b50356f Auron's Inspiration (FIN) (#7566) 2025-05-15 12:34:13 +02:00
Hans Mackowiak
43d82ce1ce lf 2025-05-15 07:08:09 +02:00
tool4EvEr
c4a8765d6c 2 fixes 2025-05-14 21:27:00 -04:00
Chris H
15d49bc1b1 - Sort inventory by item type 2025-05-14 20:36:46 -04:00
Chris H
d84712b65d Lock smith prices when you click the Smith button 2025-05-14 20:36:28 -04:00
Fulgur14
eac94f7249 10 FIN/FIC cards, batch 6 (#7553) 2025-05-14 17:18:28 +02:00
rpg2014
d134c3dfcd Cleaned up the High DPI monochrome image
Cleaned up some pixels from the high dpi monochrome android icon
2025-05-14 09:09:15 -04:00
rpg2014
0a03327299 Adding monochrome icon to the android app
Added monochrome icon pngs for each mipmap size and added them to the xml configs for the icons.  The monochrome icons were made by editing the respective icon in paint.net.
2025-05-14 09:09:15 -04:00
Fulgur14
0a673aeadc 10 FIC/FIN cards, batch 4 (#7548) 2025-05-14 10:54:45 +02:00
Paul Hammerton
40f7a9a22a Edition updates: FCA, FIC, FIN, SLD 2025-05-14 09:22:30 +01:00
Paul Hammerton
e2ccf8960a Merge pull request #1 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, SLD
2025-05-14 09:18:05 +01:00
Paul Hammerton
66acc6b920 oops lands 2025-05-14 09:15:41 +01:00
Paul Hammerton
83abcf7c44 Update Final Fantasy.txt 2025-05-14 09:13:00 +01:00
Paul Hammerton
ef9d807b6e Update Final Fantasy Commander.txt 2025-05-14 09:04:54 +01:00
Paul Hammerton
83f334888e Update Final Fantasy Through the Ages.txt 2025-05-14 08:58:38 +01:00
Paul Hammerton
8359cfda35 Edition updates: FIC, FIN, SLD 2025-05-14 08:53:21 +01:00
Fulgur14
93b2d7c795 19 FIN/FIC cards + tokens/counters (#7525) 2025-05-14 07:30:38 +00:00
rwalters
ea74cc8f7f Fix enemy name (#7551)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fix a misspelling in the name of a Merfolk Warrior when being previewed before a battle
2025-05-14 06:46:00 +03:00
Fulgur14
4e16a5ea60 Update dancers_chakrams.txt (#7549) 2025-05-13 23:23:48 +02:00
Renato Filipe Vidal Santos
00325fb511 MB2: Value Town 2025-05-13 20:52:52 +02:00
Renato Filipe Vidal Santos
fe042fa6d9 FIC: 5 cards (#7541) 2025-05-13 20:24:15 +02:00
Fulgur14
6830f15859 10 FIC/FIN cards, batch 2 (#7543) 2025-05-13 20:23:43 +02:00
Renato Filipe Vidal Santos
fb3a8ce003 FIC: another 5 cards (#7545) 2025-05-13 20:21:53 +02:00
tool4ever
edc6edc44b Support for Lord Jyscal Guado (#7546) 2025-05-13 20:21:12 +02:00
tool4ever
40b3edd426 Update charismatic_conqueror.txt 2025-05-13 18:58:06 +02:00
Fulgur14
f50aea1576 10 FIC/FIN cards, batch 1 (#7542) 2025-05-13 17:53:35 +02:00
Renato Filipe Vidal Santos
4ab67b1d3b FIN: 10 more cards (#7528) 2025-05-13 17:39:48 +02:00
Chris H
a4e4525769 Remember card not spell for tibalts trickery (#7539) 2025-05-13 17:08:40 +02:00
Hans Mackowiak
1d664145e0 Update Dominaria United.txt
Note from Scyfall:

Dominaria United series of 26 tokens
Dominaria United comes with two sets of tokens: a series of 26 bearing either DMU or DMC set codes, and a separate series of 12 tokens with exclusively the DMC set code. We don't know why they did things this way. We've opted to file the entire series of 26 here regardless of set code to avoid archival conflicts.
2025-05-13 10:11:26 +02:00
Fulgur14
32355499aa Matoya, Archon Elder and Valkyrie Aerial Unit (#7535) 2025-05-13 07:18:31 +02:00
Fulgur14
a475855c4c Update cunning_azurescale_divining_dive.txt (#7540) 2025-05-13 06:52:27 +02:00
Fulgur14
598d3a4958 Update esper_origins_summon_esper_maduin.txt 2025-05-12 23:36:54 -04:00
Fulgur14
35a4053212 Update sevinnes_reclamation.txt 2025-05-12 23:36:54 -04:00
Fulgur14
9d770a9bca Esper Origins and Ishgard, the Holy See (FIN) 2025-05-12 23:36:54 -04:00
Paul Hammerton
de15ab3a71 Alchemy Rebalancing for May 13, 2025 2025-05-12 23:33:07 -04:00
rwalters
35bc80767f Fix exit to world map edge cases (Adventure) (#7522)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Attempt to band-aid two problems relating to showing a dialog to return to the main map, one in which some patrolling enemies ignore the menu restriction and another in which such enemies touching a player who is transitioning out of a dungeon causing a soft lock upon returning to the map
2025-05-12 23:28:00 -04:00
Simisays
ef28a92dff update 2025-05-12 23:15:13 -04:00
Chris H
99f3ccb8d6 Fix endlines 2025-05-12 18:03:04 -04:00
Simisays
16148ec992 10 cards (#7485) 2025-05-12 21:41:22 +03:00
rwalters
93f7987312 Fix on the hunt hole (Adventure) (#7516)
* Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner)

* Fix an edge case where the On The Hunt quest can end up failing to ever spawn an enemy to hunt

---------

Co-authored-by: Agetian <stavdev@mail.ru>
2025-05-12 13:26:49 +03:00
Paul Hammerton
c7816daaf6 Remove Explorer from archived formats and net decks (#7524) 2025-05-12 13:14:17 +03:00
Simisays
bc458ecde4 FIN 10 cards (#7526) 2025-05-12 12:13:22 +02:00
Fulgur14
e4da62c254 3 FIN/FIC cards (#7529)
* 3 FIN cards

* Update starting_town.txt
2025-05-12 09:50:07 +02:00
Fulgur14
53588dfc24 Terra, Magical Adept (#7531)
* Terra, Magical Adept
2025-05-12 09:48:44 +02:00
Renato Filipe Vidal Santos
61c11b38a0 FIN: 4 Job select cards and support (#7520)
* Update CardFactoryUtil.java

* Update Card.java

* Update Keyword.java

* Update white_mages_staff.txt

* Update dragoons_lance.txt

* Update black_mages_rod.txt

* Update summoners_grimoire.txt

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-05-12 09:47:18 +02:00
tool4ever
fdcc33198b Update rhonas_the_indomitable.txt
Closes #7530
2025-05-12 08:13:13 +02:00
Hans Mackowiak
dd7a0e99e2 ~ lf 2025-05-12 07:00:53 +02:00
Hans Mackowiak
cb0e594a6e CardEdition: add collector number for other (#7504)
* CardEdition: add collector number for other

* EditionEntry record

* Add getOtherImageKey

* Update StaticData.java

* use getOtherImageKey in getFacedownImageKey

* Update CardEdition.java

Remove findOther in favor of getOtherSet

* Update CardEdition.java

return findOther, but with Aggregates.random

* ~ move more helper images to ImageKeys
2025-05-12 06:59:20 +02:00
tool4ever
059881a7b5 Some support for Desert Cenote (#7505)
---------

Co-authored-by: tool4EvEr <tool4EvEr@>
2025-05-11 18:19:37 +00:00
Jetz
ca59ac925c Fix weird behavior in image view with certain filters 2025-05-11 10:00:31 -04:00
Fulgur14
0b5f14cd9c 5 FIN cards (#7518) 2025-05-11 10:27:38 +00:00
Paul Hammerton
1d987c25e9 Merge pull request #7523 from paulsnoops/edition-updates
Edition updates: FCA, FIC, FIN, PF25, PLG25, SLD
2025-05-11 10:43:43 +01:00
Paul Hammerton
5cb68bc2f0 Edition updates: FCA, FIC, FIN, PF25, PLG25, SLD 2025-05-11 10:36:31 +01:00
Hans Mackowiak
1715efced7 FIN: Tiered (#7517)
* Tiered: first example

* use ModeCost for both Spree and Tiered
2025-05-11 09:38:23 +02:00
Chris H
aeb39b99bb If the AI cloned your deck, don't use your deck as a reward base if there aren't more than 5 choices. 2025-05-10 09:05:54 -04:00
Renato Filipe Vidal Santos
778066a622 FIN: Y'shtola Rhul (#7514) 2025-05-10 11:04:03 +00:00
Jetz
75a056abe6 Add a parameter for battle protection RE that can be used to override it for custom types. 2025-05-09 23:19:42 -04:00
Jetz72
3915f316e2 Unused Import 2025-05-09 23:19:42 -04:00
Jetz
b52646ee7e Delete some logic that doesn't currently work 2025-05-09 23:19:42 -04:00
Jetz
cb1d48a566 Support for non-siege battles 2025-05-09 23:19:42 -04:00
Ryan Walters
733d56246e Fixing an issue in which touching the space the map occupies outside of the world map does not allow the player to move (very relevant on maps with content in the top left corner) 2025-05-09 23:18:45 -04:00
distillate personality
7902ad71d7 Update forge-gui-mobile/src/forge/adventure/data/AdventureQuestStage.java
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2025-05-09 21:54:11 -04:00
birdbath
2666de0adb Fix for 'Fetch' objectives that allows the user to 'Use' an item in their inventory at the PoI to complete the objective instead. 2025-05-09 21:54:11 -04:00
Renato Filipe Vidal Santos
c2613a03b0 FIC: Yuna, G'raha and new token (#7512) 2025-05-09 08:26:30 +02:00
Chris H
b077cca3ac Fix line endings 2025-05-08 22:09:22 -04:00
Fulgur14
43fc281e65 Hildibrand Manderville (FIC) 2025-05-08 20:25:13 +00:00
Fulgur14
7b1f5d0a34 Zenos yae Galvus // Shinryu, Transcendent Rival (FIN) (#7503) 2025-05-08 20:05:13 +00:00
Paul Hammerton
330fe0a0a8 Merge pull request #7502 from paulsnoops/edition-updates
Edition updates: FIC, PURL, SLD
2025-05-07 18:08:15 +01:00
Paul Hammerton
30a4c24c35 Edition updates: FIC, PURL, SLD 2025-05-07 18:01:06 +01:00
Renato Filipe Vidal Santos
01a6a38285 YTDM: 4 cards (#7473) 2025-05-07 05:17:38 +00:00
Renato Filipe Vidal Santos
fe5aa37791 Update big_play.txt (#7500) 2025-05-07 06:43:39 +02:00
Simisays
42f1fba7c4 Adventure 3 small fixes (#7499)
* Update grolnok_f1.tmx

* update
2025-05-06 23:42:03 +03:00
autumnmyst
0c6f1ff58f UNF: Added the 4 eternal-format-legal dice modification/reroll cards (#7489) 2025-05-06 17:13:52 +00:00
Hans Mackowiak
8678b5ec5b Update Duskmourn House of Horror.txt
Moved to "other" section
2025-05-06 10:35:59 +02:00
Hans Mackowiak
71256972c4 Update Commander 2013.txt
remove token list
2025-05-05 22:32:37 +02:00
matthias8422
a76f23fc25 feature/Refactored-server-url-parsing-logic (#7496)
* HostPort now properly returns -1 when no port was specified

* refactored the URL parsing to now attempt a DNS lookup in cases where URI parsing fails
This now allows local computer names and localhost etc
2025-05-05 17:10:37 +00:00
Hans Mackowiak
3c7993d640 Update Secret Lair Drop Series.txt
Update token collector number and artist
2025-05-05 10:43:03 +02:00
Hans Mackowiak
71d984a5ff Update Archenemy.txt
reorder card list
2025-05-05 10:41:35 +02:00
Hans Mackowiak
be171c011a Volrath's Curse and Lost in Thought with Modes and IgnoreEffectCost (#7492)
* Refactor Damping Engine
2025-05-04 16:55:38 +00:00
tool4ever
becdd180f4 Some cleanup (#7493)
* Some cleanup

* Update StaticAbilityCantAttackBlock.java

* Some cleanup

---------

Co-authored-by: tool4EvEr <tool4EvEr@>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-05-04 18:11:00 +02:00
tool4ever
41efdb095a Update StaticAbilityCantAttackBlock.java 2025-05-04 12:25:24 +00:00
tool4ever
86453a6fa7 Update StaticAbilityCantAttackBlock.java 2025-05-04 12:24:22 +00:00
Hans Mackowiak
8448a6e20e Update cards with Static CantAttack or CantBlock (#7435)
* Update cards with Static CantAttack or CantBlock

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-05-04 13:14:58 +02:00
Hans Mackowiak
194662c9c7 StaticAbilityMode: add Enum and allow MultiMode (#7491) 2025-05-04 10:56:27 +02:00
Fulgur14
d3a33a0092 Update tifa_martial_artist.txt (#7488) 2025-05-03 21:12:06 +00:00
tool4ever
2bab13247c Some fixes (#7484) 2025-05-03 19:31:47 +02:00
Hans Mackowiak
da30b886c9 fix Card from Unknown Set calling getCardEdition 2025-05-03 16:47:39 +02:00
Hans Mackowiak
da38641ff8 Add CantBlock Mode (#7452) 2025-05-03 15:21:08 +02:00
Chris H
fcbc83c3ff Scryfall randomly changed the collector numbers 2025-05-02 16:03:25 -04:00
Hans Mackowiak
4ae93980b6 ~ remove last Unknown Artist 2025-05-02 09:42:04 -04:00
Hans Mackowiak
10b96138c3 ~ update other editions and remove wrong tokens 2025-05-02 09:42:04 -04:00
Hans Mackowiak
8706edbb01 CardEdition: have Token look for other Sets too 2025-05-02 09:42:04 -04:00
Renato Filipe Vidal Santos
4f5c074fe6 YTDM: 3 cards (#7448)
* Add files via upload

* Update loch_larent.txt
2025-04-30 19:55:06 +03:00
Hans Mackowiak
50ca71fc87 Update Return to Ravnica.txt 2025-04-30 13:24:44 +02:00
Hans Mackowiak
84134341e3 Update Bloomburrow Commander.txt
Update offspring token info
2025-04-30 09:58:39 +02:00
Hans Mackowiak
988518284c Update Bloomburrow.txt
Update offspring token info
2025-04-30 09:52:24 +02:00
Hans Mackowiak
1476621df1 CopyPermanentEffect: simplify getting PaperCard from DefinedName 2025-04-30 07:03:26 +02:00
Hans Mackowiak
cb8e13e933 Update Murders at Karlov Manor Commander.txt 2025-04-29 09:39:37 -04:00
Chris H
ea4d878a2d Update forge-gui/res/editions/Murders at Karlov Manor Commander.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
eeafc6400c Update Commander Masters.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
875b4557b0 Update Commander Anthology Volume II.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
bec25c68f2 Update Commander Anthology.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
f98aae35a5 Update Commander Legends Battle for Baldur's Gate.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
b1acb9f0a3 Update Commander Legends.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
89049db6a0 Update Modern Horizons 2.txt 2025-04-29 09:39:37 -04:00
Hans Mackowiak
77378855e3 Update Tarkir Dragonstorm Commander.txt 2025-04-29 09:39:37 -04:00
Paul Hammerton
709582086f Edition token section fixes 2025-04-29 09:39:37 -04:00
Paul Hammerton
7eda9e204e Merge pull request #7468 from paulsnoops/ytdm-formats
Format updates: YTDM
2025-04-29 11:29:43 +01:00
Paul Hammerton
cf94d96d95 Merge pull request #7467 from paulsnoops/edition-updates
Edition updates: FIC, SLD, UNK, YTDM
2025-04-29 11:29:31 +01:00
Paul Hammerton
a78d361b2e FIC 2025-04-29 11:23:53 +01:00
Paul Hammerton
d01619c09c UNK 2025-04-29 11:19:05 +01:00
Paul Hammerton
fd76098765 Format updates: YTDM 2025-04-29 11:13:51 +01:00
Paul Hammerton
9fb08f1695 Edition updates: SLD, YTDM 2025-04-29 11:09:12 +01:00
Hans Mackowiak
4eef86daf1 Card: add FacedownImageKey for better logic and set codes (#7459)
* Card: add FacedownImageKey for better logic and set codes
2025-04-29 07:32:51 +02:00
matthias8422
d74d2da755 Added a new Couldnt connect to server error message
Re wrote the URL parsing logic again, but due to increased complexity put it into a utility class
Fixed desktop version opening up a lobby even when it did not connect to a server
Wired up the new error message to mobile, and both error messages to Desktop
2025-04-28 17:56:08 -04:00
Simisays
f69fd12ebd Update quests.json
xira fix as well
2025-04-28 16:07:29 -04:00
Simisays
d41cdca1bd update 2025-04-28 16:07:29 -04:00
Chris H
acfc413465 Fix AI targeting illegally for each player destroy targeting (#7463) 2025-04-28 07:22:59 +03:00
Agetian
7a2ef6c9fd Update LDA data for Tarkir: Dragonstorm, add puzzle PS_TDM3 (Tarkir: Dragonstorm 03) (#7461)
* - Update LDA for Tarkir: Dragonstorm

* - Add puzzle PS_TDM3.
2025-04-28 07:16:06 +03:00
Chris H
b214b6f8bf Update font-list.txt 2025-04-26 23:46:01 -04:00
Chris H
4d75f7b225 Update fonts location 2025-04-26 23:46:01 -04:00
Chris H
b7db4b4cbc Fix if collector number is null 2025-04-26 22:40:43 -04:00
Chris H
efaca50d97 Fix accidentally removed other blocks 2025-04-26 22:40:43 -04:00
Chris H
0157113103 M-Z token fixes 2025-04-26 22:40:43 -04:00
Chris H
bf70d8e6f6 A-M token updates 2025-04-26 22:40:43 -04:00
Hans Mackowiak
30ac5ae381 Update Duskmourn House of Horror.txt
add artist
2025-04-26 22:40:43 -04:00
Chris H
dd94169c9a Download token images when token collector numbers are defined 2025-04-26 22:40:43 -04:00
Dave
4c24e7248d MTGO Cube 2025 04 (#7449)
* MTGO Vintage Cube 2025-04

* MTGO Vintage Cube 2025-04.draft

* MTGO Vintage Cube 2025-04.dck

* Update MTGO Vintage Cube 2025-04.dck

Use more standard versions of cards
2025-04-26 09:21:18 +03:00
Renato Filipe Vidal Santos
40eb8de90c Fixing Ravenous Harpy, Reaper of Flight Moonsilver (#7447) 2025-04-23 19:07:00 +00:00
Jetz
8f60973d95 Put back blank collector number suffix. 2025-04-23 12:48:03 -04:00
Jetz
bc9df8872f Added support for amount in give card command.
Added 'give print' command.
2025-04-23 12:48:03 -04:00
Renato Filipe Vidal Santos
1071089879 Cleanup: "During your turn" Oracle update, living metal (#7446) 2025-04-23 16:39:30 +02:00
Renato Filipe Vidal Santos
28ae7fa729 Cleanup: NumAtt$ & NumDef$, new cards 2025-04-23 11:01:27 +02:00
Hans Mackowiak
e9c5bc46ae CardEdition: add TokenInSet 2025-04-22 19:51:52 -04:00
Paul Hammerton
9308dac257 Merge pull request #7442 from paulsnoops/cmdr-bnr-22-apr-25
Commander Banned and Restricted Announcement for April 22, 2025
2025-04-22 20:34:36 +01:00
Paul Hammerton
f5fa87410b Commander Banned and Restricted Announcement for April 22, 2025 2025-04-22 20:29:07 +01:00
Renato Filipe Vidal Santos
c3167a9928 Cleanup: Optional wheel update 2025-04-22 10:41:29 +02:00
Pedro Durán
c533bee4b3 Update card translations (#7126) 2025-04-22 10:20:14 +02:00
matthias8422
01a3b2723f Fixed bug where android boolean settings were not properly displaying their checked state (#7440) 2025-04-21 17:53:53 +00:00
Hans Mackowiak
8640a2aaad ~ lf 2025-04-21 16:45:07 +02:00
Paul Hammerton
9f5e5f3068 Update Planeswalker Championship Promos.txt 2025-04-21 10:10:26 -04:00
Paul Hammerton
3db209f128 Update Media and Collaboration Promos.txt 2025-04-21 10:10:26 -04:00
Chris H
351a94281c Remove language code if we can't find it the first request 2025-04-21 10:10:26 -04:00
Chris H
f190e7e678 Add the ability to use hyphens in Collector numbers 2025-04-21 10:10:26 -04:00
Fulgur14
e01d3e0a6d Tifa, Martial Artist 2025-04-21 09:59:38 +00:00
Chris H
cb3fcb259f Revert "Add the ability to use hyphens in Collector numbers"
This reverts commit 0940e7433b.
2025-04-20 17:12:23 -04:00
Chris H
0940e7433b Add the ability to use hyphens in Collector numbers 2025-04-20 17:10:10 -04:00
tool4ever
8e48ff3dfa Update zenith_festival.txt 2025-04-20 17:57:07 +00:00
matthias8422
07b10f736b Feature/network improvement android (#7418)
* initial network improvements for mobile

* Fixed settings menu message referencing proper localization string
2025-04-20 17:34:24 +00:00
Drecon84
d0e4e5bd17 [Adventure] Quest order fix 2025-04-20 12:32:10 -04:00
Hans Mackowiak
fd082ffabb Fix implements IPref 2025-04-20 14:28:47 +02:00
Drecon84
520dcb88d6 [Adventure] Fixing fallback dialog in flower cave
This piece of code didn't work and produced a fallback dialog. Fixed the formatting to make it work as it was supposed to.
2025-04-19 22:44:37 -04:00
Jetz
a4138ebd4d Fix cards reverting to first art in set when game is restarted 2025-04-19 13:15:29 -04:00
tool4ever
1118f5b135 Fix UntilHostLeavesPlay corner case (#7430) 2025-04-19 14:35:07 +00:00
Renato Filipe Vidal Santos
e78f921e4d Update nantuko_slicer.txt (#7421) 2025-04-19 09:34:21 +00:00
tool4ever
a6ec235052 Fix Escalate (#7428) 2025-04-19 08:09:41 +00:00
Jetz72
613cc549f7 Update DraftEffect.java
Delete extra if statement
2025-04-18 19:21:37 -04:00
Jetz72
69c7083592 Update forge-game/src/main/java/forge/game/ability/effects/DraftEffect.java
Co-authored-by: Chris H <zenchristo@gmail.com>
2025-04-18 19:21:37 -04:00
Jetz
7f64e25691 Disable TokenCard flag on drafted cards by default. 2025-04-18 19:21:37 -04:00
Justin Babcock
2cb107380d Fix Knockout Maneuver wrong card type (#7425) 2025-04-18 20:13:36 +00:00
Drecon84
2a4eaea90a [Adventure] Fix for beating castles before getting there in main quest (#7391)
* First draft of the castle main quest bug workaround

Adds a possibility to start the game without the main quest and changes all castles to only let you into the gate if you either don't have a main quest active or are actively on the quest to defeat the castles.

* Rewrote main quest stuff

Wanted to decouple the main quest from the start of the game a little bit to allow the player to start without a main quest.

* Fully tested solution for main quest bug

Castles should now behave as expected.
Need to fix one minor bug still.

* Fixing the starting portal and wizard

This seems to have completely fixed the issues I have found with not starting with a main quest. Hopefully I have found all of the things that might break.
2025-04-18 12:54:09 -04:00
Paul Hammerton
8fdbaf3611 Merge pull request #7423 from paulsnoops/fix-sld
Edition updates: SLD
2025-04-18 09:19:06 +01:00
Paul Hammerton
55edec665f Edition updates: SLD 2025-04-18 09:15:41 +01:00
Paul Hammerton
f11c6a7038 Merge pull request #7422 from paulsnoops/edition-updates
Edition updates: PF25, SLD
2025-04-18 09:09:11 +01:00
Paul Hammerton
36150be7f3 Edition updates: PF25, SLD 2025-04-18 09:04:07 +01:00
Hans Mackowiak
b861994d2f add missing Enchant Keywords 2025-04-18 08:52:28 +02:00
matthias8422
0242189012 Fixed mobile server host UPnP dialog from accidently blocking ther GUI thread indefinatly (#7417) 2025-04-18 06:43:31 +00:00
tool4ever
69ddfdc18c Update envoy_of_the_ancestors.txt
Closes #7419
2025-04-18 06:39:56 +00:00
Hans Mackowiak
bef56d1caa add missing Enchant Keywords 2025-04-18 08:38:36 +02:00
tool4ever
c7f43e245d Fix missing LTB trigger (#7415) 2025-04-18 06:36:18 +00:00
Hans Mackowiak
939b7a22e0 Update fog_on_the_barrow_downs.txt 2025-04-18 06:12:52 +02:00
Jetz
a79a3503f5 Make PaperCardFlags serializable 2025-04-17 20:14:37 -04:00
NicolasCunha
9109207495 fix: concede shortcut not working due to event dispatch thread 2025-04-17 12:48:26 -04:00
Hans Mackowiak
dbac513027 Update tourachs_gate.txt 2025-04-17 16:50:31 +02:00
Hans Mackowiak
3152ac93f6 Update caribou_range.txt 2025-04-17 16:50:16 +02:00
Hans Mackowiak
fc56076f81 Update mystic_might.txt 2025-04-17 16:49:49 +02:00
Hans Mackowiak
ecc763faf6 Update hot_springs.txt 2025-04-17 16:48:50 +02:00
Hans Mackowiak
0130367873 Update bestial_bloodline.txt 2025-04-17 16:44:12 +02:00
Hans Mackowiak
c36b9b7610 Update ice_cage.txt 2025-04-17 16:18:30 +02:00
Renato Filipe Vidal Santos
774ab38335 Update nantuko_slicer.txt (#7413) 2025-04-17 15:34:10 +02:00
tool4ever
251ce2f83b Fix Torment of Hailfire in multiplayer (#7412)
Co-authored-by: TRT <>
2025-04-17 11:55:41 +03:00
Hans Mackowiak
2af47fe6df fix 'Land you control' auras 2025-04-17 06:43:04 +02:00
Hans Mackowiak
332c982695 Update crackling_emergence.txt
Fix Enchant
2025-04-17 05:58:03 +02:00
Hans Mackowiak
eb970665a6 Update harmonious_emergence.txt
Fix Enchant
2025-04-17 05:57:08 +02:00
Jetz72
4714319204 Store cards by collectorNumber instead of artIndex; PaperCard flag support (#7240)
* Refactor - Unknown set code to constant

* Refactor - Support for multiple initial selections for getChoices

* Covert noSell and marked color identities into serializable flags

* Fix cards in deck not being converted to newer noSell format

* unused imports

* Fix NPE

* Cleanup card filter

* Remove 14-year-old check that shouldn't be possible anymore

* CRLF -> LF

---------

Co-authored-by: Jetz <Jetz722@gmail.com>
2025-04-16 18:59:31 -04:00
tool4ever
c85214b9e3 Fix always targeting Graveyard with Aura (#7408) 2025-04-16 21:03:22 +00:00
tool4ever
d5854c9d1c Fix Artifact Possession (#7409) 2025-04-16 21:03:09 +00:00
Renato Filipe Vidal Santos
a3a2a5dd1b Update Reanimate, Kotis (#7403) 2025-04-16 11:22:23 +02:00
Chris H
8f155c9cca Verify we can still target before we add the target to our list (#7404) 2025-04-16 07:15:10 +02:00
matthias8422
366ed643a7 Feature/network improvements (#7365)
* Initial commit of network improvements
Seperated server properties into their own file, this will eventually help facilitate a headless server.
Fixed the localhost ip mapping to give the correct IP Address instead of failing and defaulting to "localhost"
Fixed UPnP as well as added some additional options and choices regarding UPnP
Added localization strings to all language files. (Translators will need to translate these, but the current English string is there for easy reference so they dont have to search the en-US file)

* Initial commit of network improvements
Seperated server properties into their own file, this will eventually help facilitate a headless server.
Fixed the localhost ip mapping to give the correct IP Address instead of failing and defaulting to "localhost"
Fixed UPnP as well as added some additional options and choices regarding UPnP
Added localization strings to all language files. (Translators will need to translate these, but the current English string is there for easy reference so they dont have to search the en-US file)

* Fixed properties file reference

* Refactored server address parsing logic to use the Java URI class for improved readability and robustness.
Extracted reusable code into separate functions to enhance modularity and maintainability.
General code cleanup to improve structure and readability.

* Fixed a potential issue if a protocol was already specified in the connection url.

* Removed logger implementation as changing loggers is out of scope for this PR
Reverted to JUPnP as its implementation is fixed in #7367
Made some of the new localization strings generic as they can be used elsewhere
Added a server.preferences.example file
removed the server port from the old location (forge.progile.properties.example)
Added a server port back into ForgeConstants as it doesnt make sense for the prefered hosting port of the user to override the default Forge connection port.

* resolve conflicts between this branch and master

* Implemented a parent class for all preference Enums so they can be passed into a function regardless of type using IPref, necessary since I separated server settings into its own FNetPref file
Added server preferences section to the preferences Desktop GUI
Added a port preference setting and a UPnP preference setting to the aforementioned server preferences section
Added a localizedComboBox and localizedComboBoxListener so that localized strings can be used in combobox dropdowns in the server preferences section.

TODO: (In scope)
The new server preferences section needs to be added to Android and IOS perhaps?

TODO: (out of scope)
GamePlayerUtil has a bunch on non localized english strings that should be converted to localized

* Fixed unused import

* Resolved merge conflicts
Added server settings to the reset to defaults function
2025-04-15 20:32:32 -04:00
tool4ever
e7becacd57 Fix StoreVoteNum (#7402)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-04-15 18:47:24 +02:00
Hans Mackowiak
2cf9293ab3 AttachAI: remove getFirstAttachSpell (#7400)
* AttachAI: remove getFirstAttachSpell

* Update GameAction.java

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-04-15 11:55:38 +03:00
Hans Mackowiak
88300de6e5 Update aligned_heart.txt
remove keywords
2025-04-15 07:02:53 +02:00
tool4ever
a4cb0924f3 Fix World rule (#7397) 2025-04-14 14:59:51 +00:00
Hans Mackowiak
e0001f8348 Update TokenAi, remove getFirstAttachSpell (#7398)
Removes getFirstAttachSpell
2025-04-14 16:47:53 +02:00
tool4ever
e380590c4c Fix Old-Growth Troll (#7396)
* Fix Old-Growth Troll

* Update GameAction.java

Add AI SVars to EmptySA

* Fix logic

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-04-14 15:24:09 +02:00
Hans Mackowiak
7410a2844f Update dance_of_the_dead.txt
Remove NewAttach
2025-04-14 14:45:54 +02:00
tool4ever
b4af62eda7 CantUntap Third Step (#7393) 2025-04-13 12:49:07 +00:00
tool4ever
9c14ba49a3 Fix Licids being able to reattach without ending the effect first (#7394) 2025-04-13 12:43:22 +00:00
Hans Mackowiak
25900ee10c Aura Spells have internal Attach Spell for multiple Enchant Keywords (#6996)
* Aura Spells have internal Attach Spell for multiple Enchant Keywords

* AttachAI: add logic for Reanimate Aura with AnimateAI
2025-04-13 13:36:31 +02:00
tool4ever
443ba2c1e0 Upgrade Jetty (#7390) 2025-04-12 21:47:04 +00:00
Hans Mackowiak
6ade60cd59 removed getDirectSVars (#7389) 2025-04-12 21:15:54 +02:00
tool4ever
436697e0b3 Update stadium_headliner.txt
Closes #7388
2025-04-12 14:29:52 +00:00
Hans Mackowiak
e7d8664386 Svar fallback changes (#7385)
* getSVarFallback use stream filter to find one with name
2025-04-12 10:44:19 +02:00
tool4ever
e2e3c658a0 Relocate android specific dependency (#7387) 2025-04-12 05:47:07 +00:00
tool4ever
2a4ea4cb5d Some fixes (#7386) 2025-04-11 20:40:35 +00:00
tool4ever
93215e6ce4 Fix Distended Mindbender (#7381) 2025-04-11 16:02:47 +02:00
tool4ever
f4a0de6392 JUPnP android fix (#7379) 2025-04-11 08:55:40 +02:00
Renato Filipe Vidal Santos
579033abb3 Update whirlwing_stormbrood_dynamic_soar.txt (#7377) 2025-04-10 19:32:01 +00:00
Renato Filipe Vidal Santos
8328866645 Cleanup: April 2025, pass 3 2025-04-10 16:44:28 +02:00
Hans Mackowiak
890ce2505c Unearth: add gain Haste as StaticLayer (#7369)
* Unearth: add gain Haste as StaticLayer
2025-04-10 16:40:37 +02:00
tool4ever
7e6697d100 Fix checking with LKI during combat (#7372) 2025-04-10 15:43:22 +02:00
Agetian
84d793b786 Add 4 Possibility Storm puzzles (#7371)
* - Add TDM/TDC achievements by Marek14.

* - Add 4 Possibility Storm puzzles.
2025-04-10 11:55:42 +03:00
tool4ever
386550da39 Fix JUPnP initialization (#7367)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-04-10 08:55:47 +03:00
tool4ever
6d3d11398d Update rite_of_renewal.txt 2025-04-09 15:46:27 +00:00
Greg Sonnier
5db9c189ba Fixed Renew cost in alchemists_assistant.txt (#7363) 2025-04-08 20:59:04 +00:00
Hans Mackowiak
8d3e3cb253 AttachAi: make KeepTapped logic generic (#7361) 2025-04-08 20:54:09 +00:00
Agetian
2042aaa033 - Add TDM/TDC achievements by Marek14. (#7362) 2025-04-08 10:09:24 +03:00
tool4ever
6fc5d218c9 Update savior_of_the_sleeping.txt 2025-04-07 17:27:34 +00:00
Hans Mackowiak
6332f5cbfc Update champion_of_dusan.txt
Fix Renew Cost
2025-04-07 17:22:37 +02:00
Chris H
44340998fd Update host_of_the_hereafter.txt 2025-04-07 09:30:00 -04:00
Chris H
5274c976ef Restore flatten plugin 2025-04-06 12:13:08 -04:00
GitHub Actions
280d2fed6d [maven-release-plugin] prepare for next development iteration 2025-04-06 12:13:08 -04:00
GitHub Actions
9ecea646d2 [maven-release-plugin] prepare release forge-2.0.03 2025-04-06 12:13:08 -04:00
Chris H
c4e5101a42 Update maven-publish for releases without deploying to FTP 2025-04-06 12:13:08 -04:00
Chris H
48a555817d Temporarily remove flatten plugin for release 2025-04-06 12:13:08 -04:00
Renato Filipe Vidal Santos
2cc2ae421a Cleanup: April 2025, pass 2 (#7350)
* Add files via upload

* Add files via upload

* Add files via upload

* Add files via upload

* Update furious_forebear.txt
2025-04-06 17:05:44 +03:00
Paul Hammerton
9109b26484 TDM: Humbling Elder (#7355) 2025-04-06 07:45:55 +00:00
Hans Mackowiak
98e5eb9652 Adventure and Omen as ReplacementEffect in CardState (#7347) 2025-04-05 17:58:20 +00:00
tool4ever
4e93f95dff Update furious_forebear.txt 2025-04-05 17:48:51 +00:00
Renato Filipe Vidal Santos
44e1332fb4 Cleanup: April 2025, pass 1 2025-04-05 17:02:21 +00:00
Hans Mackowiak
e2972acad0 Put ForColor for Call of the Spirit Dragons (#7348)
* Support for Call the Spirit Dragons

* ~ better ChoiceTitle

* ~ add AI flags
2025-04-05 17:14:43 +02:00
Renato Filipe Vidal Santos
76086ffd35 Cleanup: Updating to Count$Valid, pass 4 2025-04-05 06:47:13 +00:00
tool4ever
3c3c47616b Update UnattachEffect.java
Closes #7345
2025-04-05 08:37:53 +02:00
tool4ever
de5a660a6a Update thought_lash.txt
Fix exiling own cards when stolen
2025-04-04 21:04:54 +00:00
Drecon84
d348a72ae4 Excluding more enemy types
These are also not suited for the tutorial. Excluding these as well.
2025-04-04 10:41:17 -04:00
Fulgur14
4055512698 Temur Roar deck 2025-04-04 10:37:56 -04:00
Renato Filipe Vidal Santos
b4b2346fb8 Cleanup: Updating to Count$Valid, pass 3 (#7342) 2025-04-04 11:20:44 +02:00
Paul Hammerton
caa5443874 Update tdm.rnk 2025-04-04 09:13:43 +01:00
Renato Filipe Vidal Santos
c7fb0bd3c3 Cleanup: Count$InYour[Zone], part 2 (#7325) 2025-04-04 09:58:01 +02:00
Renato Filipe Vidal Santos
1f5f62b21a Cleanup: Count$InYour[Zone], part 1 (#7324) 2025-04-04 09:57:21 +02:00
Chris H
04937b6447 Migrate upcoming card scripts 2025-04-03 23:36:00 -04:00
Chris H
94ea88b171 Migrate upcoming card scripts 2025-04-03 23:33:19 -04:00
Chris H
2188582e16 Add initial booster+set info for TDM 2025-04-03 23:19:46 -04:00
Drecon84
fbc73fa22b Small fix to clean up the code
Combining ifs that do not need to be split up.
2025-04-03 16:16:39 -04:00
Drecon84
31790c3dc9 Remove Quest items on NG+
This removes quest items from a NG+ run in adventure mode. It might not remove all when a file has multiple NG+'s already, but multiple resets will remove all of the items.
I decided to remove the quest item tag from the teleport runes, since they are not added by a quest but can be bought in the store instead. Keeping them seems a good move for NG+.
2025-04-03 16:16:39 -04:00
Hans Mackowiak
cdf3038ab6 Harmonize first try (#7321)
* Harmonize first try

* Use OptionalCost

* Keyworded don't need type

* TDM/TDC: 11 harmonize cards (#7323)

* Trickery extrinsic fix

* Split fix

* Annoying checks keep failing

* Fix logic

* Clean up

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Renato Filipe Vidal Santos <45150760+dracontes@users.noreply.github.com>
Co-authored-by: TRT <>
2025-04-03 19:37:16 +02:00
tool4EvEr
ff1781b734 Fix creating token with Endure 0 2025-04-03 11:22:11 +02:00
Hans Mackowiak
49d7351eac ~ lf 2025-04-03 06:11:26 +02:00
Paul Hammerton
3b372eead7 Merge pull request #7330 from paulsnoops/edition-updates
Edition updates: FIN, SLD
2025-04-02 18:05:22 +01:00
Paul Hammerton
c815f7ed0c Edition updates: FIN, SLD 2025-04-02 17:59:44 +01:00
Fulgur14
04ac8d8de9 Gladiolus Amicitia (FIN) (#7329) 2025-04-02 15:42:57 +02:00
Renato Filipe Vidal Santos
6b961ed3e1 Update monstrous_rage.txt (#7326) 2025-04-02 08:34:22 +02:00
Renato Filipe Vidal Santos
17a2a23981 Cleanup: Count$TypeInYour[Zone].[Type] (#7312) 2025-04-01 11:01:45 +00:00
Renato Filipe Vidal Santos
74e83f2a94 Cleanup: Targeted effects that grant flashback (#7319) 2025-04-01 11:00:39 +00:00
tool4ever
8e25dd0e25 Clean up (#7322) 2025-04-01 08:46:15 +00:00
Drecon84
7f9260f54c WASD Adventure take 3
Now actually putting the keybinds back in. Forgot to unrevert previously.
2025-03-31 21:24:31 -04:00
Paul Hammerton
505d4b4f9a Merge pull request #7316 from HeitorBittenc/Alchemy-boosters-draft
Alchemy sets boosters added
2025-03-31 17:30:35 +01:00
Paul Hammerton
baca196066 Merge pull request #7317 from Fulgur14/Fulgur14-patch-452548
Mardu Surge deck
2025-03-31 17:30:27 +01:00
Paul Hammerton
b08cf3bf5b Merge pull request #7318 from paulsnoops/b-and-r-2025-03-31
Banned and Restricted Announcement for March 31, 2025
2025-03-31 17:29:49 +01:00
Paul Hammerton
b5ee3eb43c Banned and Restricted Announcement for March 31, 2025 2025-03-31 17:24:53 +01:00
Fulgur14
081e1a2960 Mardu Surge deck 2025-03-31 17:45:15 +02:00
Hans Mackowiak
d0310e257b Mobilize X: enable the keyword to pass the SVars to the static (#7310) 2025-03-31 09:32:38 +00:00
Renato Filipe Vidal Santos
a3733f1fa8 TDM: Avenger of the Fallen 2025-03-31 08:20:34 +00:00
HeitorBittenc
b4bd7947f9 Alchemy sets boosters added 2025-03-30 23:05:31 -03:00
Renato Filipe Vidal Santos
b56da433eb Update hunters_bow.txt 2025-03-30 16:20:07 +02:00
Hans Mackowiak
64b5906f08 lf 2025-03-30 13:09:12 +02:00
tool4EvEr
f7d94fabc9 Fix Damping Engine crashing 2025-03-30 11:27:39 +02:00
HeitorBittenc
87e0810603 Adventure Mode: Fix Waste town generic Equipment shop not appearing 2025-03-29 22:24:04 -04:00
Fulgur14
93f2fa8f43 Update Abzan Armor [TDC] [2025].dck 2025-03-29 20:47:00 -04:00
Fulgur14
c86bf402fd Update Jeskai Striker [TDC] [2025].dck 2025-03-29 20:47:00 -04:00
Drecon84
9926004cf1 Fix for main quest
This fixes the bug where the objective to free the wizard from the red castle doesn't complete.
2025-03-29 20:25:45 -04:00
Renato Filipe Vidal Santos
d0c24f49a9 Update captain_america_first_avenger.txt 2025-03-29 14:56:01 -04:00
Renato Filipe Vidal Santos
661f3b8e7a Update captain_america_first_avenger.txt 2025-03-29 14:56:01 -04:00
Chris H
14249150a0 Fix trailing comma in json 2025-03-29 14:55:08 -04:00
churrufli
93dccdeace Net Decks Archive Updates (#7283) 2025-03-29 19:23:58 +03:00
Renato Filipe Vidal Santos
706ef4ac6c TDM: 14 Omen-related cards (#7300) 2025-03-29 16:13:35 +00:00
Paul Hammerton
bd3994a217 Merge pull request #7302 from paulsnoops/edition-updates
Edition updates: PF24, PMEI, PSPL, PW25, SCH, SLP
2025-03-29 16:07:10 +00:00
HeitorBittenc
e722c4b63c Removed token image fetch attempt from old server (#7287) 2025-03-29 19:01:29 +03:00
Paul Hammerton
a062e0040d Edition updates: PF24, PMEI, PSPL, PW25, SCH, SLP 2025-03-29 15:57:12 +00:00
Hans Mackowiak
7fdd645026 Omen: first attempt (#7297)
* Omen: first attempt

* Support rendering

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-29 16:54:20 +01:00
Paul Hammerton
3670891ec9 Merge pull request #7301 from paulsnoops/tdm-tdc-formats
Add TDM & TDC to formats
2025-03-29 14:55:58 +00:00
Paul Hammerton
6f3dd8deba Add TDM & TDC to formats 2025-03-29 14:49:32 +00:00
Chris H
63ac4a3ee4 Fix line endings 2025-03-29 09:58:37 -04:00
Hans Mackowiak
4616ee715e Update pom.xml
fix CDATA for Add-Opens
2025-03-29 09:07:54 -04:00
Paul Hammerton
5ef6bf1c15 Merge pull request #7299 from paulsnoops/fix-pp-name
Fix Poised Practitioner name
2025-03-29 12:31:28 +00:00
Paul Hammerton
862b4e19b6 Fix Poised Practitioner name 2025-03-29 12:25:05 +00:00
Paul Hammerton
378524dc39 Merge pull request #7298 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-29 12:19:24 +00:00
Paul Hammerton
6d5f45a311 Edition updates: TDC, TDM 2025-03-29 12:16:13 +00:00
Fulgur14
0e00a52eb4 Update rainveil_rejuvenator.txt (#7294) 2025-03-29 08:14:43 +00:00
tool4ever
4b69d16c6d Update conduit_of_worlds.txt 2025-03-28 22:15:50 +00:00
Renato Filipe Vidal Santos
92e17a66f2 TDM: 6 cards (#7291) 2025-03-28 20:01:40 +00:00
Fulgur14
b601431591 TDM final spoiler batch - part 3 (#7292) 2025-03-28 18:49:19 +00:00
Fulgur14
76db40189e TDM final spoilers part 1 (#7289) 2025-03-28 18:46:00 +00:00
Renato Filipe Vidal Santos
8ddf8225c0 TDM: Mardu Siegebreaker 2025-03-28 18:43:56 +00:00
Fulgur14
359dd8d641 TDM final spoiler batch, Part 2 (#7290) 2025-03-28 18:41:12 +00:00
Fulgur14
878da9b06f Warden of the Grove (TDM) (#7281) 2025-03-27 19:24:57 +00:00
Fulgur14
d843004ad6 Hundred-Battle Veteran (TDM) (#7280)
The only problem I've found is that it's not shown in the "flashback" panel for some reason.
2025-03-27 18:57:53 +00:00
Hans Mackowiak
7f6024f81f Endure effect (#7254)
* add EndureEffect

* ~ fix style

* Add files via upload

* Update dusyut_earthcarver.txt

* add better Endure Message

* Update krumar_initiate.txt

* Fix message

* - Add basic EndureAi

* - Fix imports

* Update EndureAi.java

Apply static check for the token

* Update EndureAi.java

fix import

* Add files via upload

---------

Co-authored-by: Renato Filipe Vidal Santos <45150760+dracontes@users.noreply.github.com>
Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-03-27 19:21:18 +01:00
Paul Hammerton
5a37b49fcd Merge pull request #7279 from paulsnoops/edition-updates
Edition updates: SLD, TDM
2025-03-27 18:05:14 +00:00
Paul Hammerton
1c8cdac5be Edition updates: SLD, TDM 2025-03-27 18:00:21 +00:00
Fulgur14
20bd27d487 Lotuslight Dancers and Defibrillating Current (TDM) (#7277) 2025-03-27 15:23:34 +01:00
HeitorBittenc
fc761220d2 Removed 2 additional unnecessary requests for tokens images 2025-03-27 10:19:14 -04:00
HeitorBittenc
144681012c Removed print used for tests 2025-03-27 10:19:14 -04:00
HeitorBittenc
8fe3bd3c79 Removed unnecessary request for image fetching 2025-03-27 10:19:14 -04:00
tool4ever
da65308cf2 Some fixes (#7276)
* Fix unlocked Room token
2025-03-27 14:48:01 +01:00
Fulgur14
011457e949 2 TDM commons (#7271) 2025-03-27 15:29:02 +03:00
Renato Filipe Vidal Santos
c296025837 TDM: 10 cards (#7274) 2025-03-27 12:47:17 +01:00
Chris H
ac4c501629 Fix line endings 2025-03-26 19:05:22 -04:00
Heitor Bittencourt
fe062a9312 Refactor Entry to List + add comments 2025-03-26 16:05:22 -04:00
Hans Mackowiak
65d4505b67 Update FDeckViewer.java
Use Set instead of List
2025-03-26 16:05:22 -04:00
Heitor Bittencourt
073d7e537c indentation 2025-03-26 16:05:22 -04:00
Heitor Bittencourt
77dc367c95 Fix ConcurrentModificationException by iterating over a separate list 2025-03-26 16:05:22 -04:00
Heitor Bittencourt
7553d164f4 fix: copy to clipboard now collates copies of a card together. 2025-03-26 16:05:22 -04:00
Heitor Bittencourt
ee01e3d29f fix: copy to clipboard now collates copies of a card together. 2025-03-26 16:05:22 -04:00
Paul Hammerton
58f8c39197 Merge pull request #7267 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-26 18:57:52 +00:00
Paul Hammerton
7304fa862a Edition updates: TDC, TDM 2025-03-26 18:53:10 +00:00
Fulgur14
1ef8b9ca47 Another 10 TDM cards, including Exhales (#7266) 2025-03-26 18:48:36 +00:00
tool4ever
5191d2f9c4 Safer canUntap check (#7265) 2025-03-26 17:27:18 +00:00
Fulgur14
ff74e36fe5 Kheru Goldkeeper (#7264) 2025-03-26 11:12:49 +00:00
Renato Filipe Vidal Santos
0121619e93 TDM/TDC: 10 cards (#7263) 2025-03-26 09:11:47 +00:00
Drecon84
aeb279a6f8 WASD Movement Take 2 (#7251)
* Enter now still brings up the menu, controller might need it to type. WASD typing still works.
2025-03-26 07:27:16 +00:00
Fulgur14
20815552b9 3 TDC + 1 TDM cards (#7262) 2025-03-25 18:53:59 +00:00
tool4ever
ac67a36ccf Replace first withContext call with more stable AI prediction (#7261) 2025-03-25 18:08:36 +00:00
Fulgur14
fcd8b8fd35 Thunder of Unity (TDM) (#7257) 2025-03-25 17:02:03 +00:00
Renato Filipe Vidal Santos
fbe4ad5c44 TDM: 2 cards 2025-03-25 17:10:23 +01:00
Renato Filipe Vidal Santos
ee17483fff AddPower & AddToughness: Removing redundancy 2025-03-25 15:45:32 +01:00
Renato Filipe Vidal Santos
a451f1a234 Cleanup: NumAtt$ & NumDef$, part 8 (#6885) 2025-03-25 15:01:24 +01:00
Renato Filipe Vidal Santos
52c4c01a7d Cleanup: NumAtt$ & NumDef$, part 7 (#6884) 2025-03-25 14:59:00 +01:00
Renato Filipe Vidal Santos
b1bb0d669f Cleanup: NumAtt$ & NumDef$, part 6 (#6883) 2025-03-25 14:58:47 +01:00
Renato Filipe Vidal Santos
eb1f9783aa Cleanup: NumAtt$ & NumDef$, part 5 (#6882) 2025-03-25 14:58:36 +01:00
Renato Filipe Vidal Santos
0724d224fa Cleanup: NumAtt$ & NumDef$, part 4 (#6881) 2025-03-25 14:58:25 +01:00
Renato Filipe Vidal Santos
e7775cdfa9 Cleanup: NumAtt$ & NumDef$, part 3 (#6880) 2025-03-25 14:58:13 +01:00
Renato Filipe Vidal Santos
f8836f0c40 Cleanup: NumAtt$ & NumDef$, part 2 (#6879) 2025-03-25 14:57:47 +01:00
Renato Filipe Vidal Santos
4c342cfc6a Cleanup: NumAtt$ & NumDef$, part 1 2025-03-25 14:57:35 +01:00
Fulgur14
df05ab34fb 4 TDM cards (#7235) 2025-03-25 14:56:23 +01:00
Paul Hammerton
5da0e75252 Merge pull request #7255 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-25 10:07:21 +00:00
Paul Hammerton
92ec5d8f64 Edition updates: TDC, TDM 2025-03-25 09:55:59 +00:00
Renato Filipe Vidal Santos
6515fed9d2 TDM/TDC: 4 cards (#7252) 2025-03-25 07:10:37 +00:00
Chris H
5a7cd40614 Update README.md 2025-03-24 22:26:33 -04:00
Fulgur14
65b01e0822 10 TDM/TDC cards (#7249) 2025-03-24 21:19:13 +00:00
Renato Filipe Vidal Santos
643f893d43 TDM: 4 cards (#7248) 2025-03-24 20:42:02 +00:00
Paul Hammerton
e0c6b43214 Merge pull request #7247 from paulsnoops/edition-updates
Edition updates: TDC, TDM, SLD
2025-03-24 18:10:59 +00:00
Paul Hammerton
0f5d71f933 Edition updates: TDC, TDM, SLD 2025-03-24 17:54:03 +00:00
Paul Hammerton
e6a8b5ed74 Edition updates: TDC, TDM, SLD 2025-03-24 17:50:32 +00:00
LEGIONLAPTOP\dougc
24c11e47c4 Updating lang files with new strings 2025-03-24 12:39:50 -04:00
LEGIONLAPTOP\dougc
cb5f805767 Added CommanderGauntlet GameType, added CustomCommanderGauntlet to mobile 2025-03-24 12:39:50 -04:00
LEGIONLAPTOP\dougc
3788e01f38 Quick Commander and Build Commander Gauntlet both working on desktop 2025-03-24 12:39:50 -04:00
LEGIONLAPTOP\dougc
a9df4ea424 Renamed Commander Gauntlet to more accurate QUICK Commander Gauntlet 2025-03-24 12:39:50 -04:00
Chris H
25ba06d530 Revert "WASD movement for Adventure mode" (#7241)
* Revert "Improved WASD script"

This reverts commit fc901f1ebb.

* Revert "Fixed WASD movement"

This reverts commit c365f5a3d1.

* Revert "Update KeyBinding.java"

This reverts commit 49697c863c.

* Revert "Adventure Keybinds"

This reverts commit 4431c40de6.
2025-03-24 09:14:23 -04:00
tool4ever
9b81644f11 Fix Danny Pink triggering once for each type (#7245) 2025-03-24 13:09:57 +01:00
tool4ever
4b27536ed3 Update eshki_temurs_roar.txt
Closes #7238
2025-03-24 10:56:51 +01:00
Renato Filipe Vidal Santos
148da24456 TDM: 2 cards (#7244) 2025-03-24 09:19:51 +01:00
Renato Filipe Vidal Santos
d1be43fd83 TDM: Rot-Curse Rakshasa (#7242) 2025-03-24 07:06:02 +01:00
Chris H
f2df505237 Don't activate connive if Amount == 0 2025-03-23 13:14:58 -04:00
Chris H
bd37e26fab Make the AI more likely to sacrifice/chump block with Reef Worm + Spawn 2025-03-23 13:10:53 -04:00
Chris H
7954473476 FIx Line endings 2025-03-23 12:46:58 -04:00
Renato Filipe Vidal Santos
13287cefbd TDM: 5 Cards (#7234) 2025-03-23 13:48:29 +01:00
Fulgur14
9afbc91de1 1 TDC + 11 TDM cards (#7230) 2025-03-23 11:56:20 +01:00
Renato Filipe Vidal Santos
dfe5bd9ec9 TDM/TDC: 8 cards (#7223) 2025-03-23 10:11:28 +01:00
Renato Filipe Vidal Santos
e2411e34bd Update shiko_and_narset_unified.txt (#7236) 2025-03-23 10:07:41 +01:00
Chris H
f2998bdf9a FIx Line endings 2025-03-22 18:36:42 -04:00
Heitor Bittencourt
c52f886e89 Adventure: Disable Not For Sale Overlay Setting 2025-03-22 18:21:58 -04:00
Heitor Bittencourt
f972aa44ba Adventure: display shop name items on boosters removed 2025-03-22 18:21:09 -04:00
Heitor Bittencourt
ccafe0557f Adventure: Replaced Booster Shop Image 2025-03-22 18:21:09 -04:00
Paul Hammerton
f9f9b1a1f9 Merge pull request #7229 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-22 10:21:34 +00:00
Fulgur14
e867aacbf5 9 TDC/TDM cards (#7225) 2025-03-22 11:12:21 +01:00
Paul Hammerton
a09e9e4fd6 Edition updates: TDC, TDM 2025-03-22 10:10:35 +00:00
Renato Filipe Vidal Santos
fc320e6524 Fixing Will of the Mardu (#7226) 2025-03-22 08:08:48 +01:00
Chris H
2b6a1c9f3d Refilter targetable list while looping the target selections 2025-03-21 19:11:01 -04:00
Chris H
3e9cd2c226 Fix some adventure issues 2025-03-21 19:11:01 -04:00
Paul Hammerton
3f722abba2 Merge pull request #7224 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-21 18:01:50 +00:00
Paul Hammerton
06508f70a3 Edition updates: TDC, TDM 2025-03-21 17:42:54 +00:00
Fulgur14
8580108d1e 10 TDM cards (Sidisi and Sibsigs) (#7190) 2025-03-21 10:11:10 +01:00
Hans Mackowiak
300f34377c Update aligned_heart.txt
fix trigger
2025-03-21 09:35:41 +01:00
Renato Filipe Vidal Santos
c4828f510f TDC: 4 cards (#7221) 2025-03-21 09:06:50 +01:00
Fulgur14
04c400553a Another 10 TDM/TDC cards (#7220) 2025-03-21 08:36:50 +01:00
tool4ever
d2508333bc Fix NPE: Jacob Frye + Escape Detection (#7219)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-21 08:31:09 +03:00
Drecon84
fc901f1ebb Improved WASD script
Now the script works with arrays, much cleaner. I have not been able to test the controller support, it should work but I don't have the means to test it.
2025-03-20 20:35:54 -04:00
Drecon84
c365f5a3d1 Fixed WASD movement
Adventure moves with WASD now
2025-03-20 20:35:54 -04:00
Drecon84
49697c863c Update KeyBinding.java 2025-03-20 20:35:54 -04:00
Drecon84
4431c40de6 Adventure Keybinds
Can now move in adventure mode with WASD keys
2025-03-20 20:35:54 -04:00
Renato Filipe Vidal Santos
a0f6efb959 TDM: 6 cards 2025-03-20 21:02:06 +00:00
tool4ever
44fea0ae75 Fix AI running into timeout keeping thread running (#7215) 2025-03-20 18:17:41 +00:00
Paul Hammerton
95c970e23f Merge pull request #7217 from paulsnoops/edition-updates
Edition updates: TDC, TDM
2025-03-20 17:43:54 +00:00
Paul Hammerton
8596151fa1 Edition updates: TDC, TDM 2025-03-20 17:32:44 +00:00
Paul Hammerton
2eac43734c Merge pull request #7216 from paulsnoops/master
LF
2025-03-20 17:29:09 +00:00
Paul Hammerton
dff91eb2aa LF 2025-03-20 17:25:12 +00:00
Renato Filipe Vidal Santos
aa122700a9 TDC: 4 cards (#7208) 2025-03-20 17:52:47 +01:00
Fulgur14
80f267df59 Dalkovan Packbeasts (TDM) (#7214) 2025-03-20 13:45:55 +01:00
tool4ever
235618c3bb Fix Valiant tracking incorrectly with controller change (#7200)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-20 15:26:53 +03:00
Fulgur14
b7e55e785e Perennation (TDM) (#7213) 2025-03-20 13:24:25 +03:00
Fulgur14
050c986d08 A few more TDM/TDC cards (#7207) 2025-03-20 10:34:16 +01:00
Fulgur14
2b83541ebc 9 TDM + 1 TDC card (Eshki, Temur's Roar and friends) (#7198) 2025-03-20 08:37:17 +01:00
Fulgur14
d40894ef6a Tempest Hawk + 3 TDC cards (#7202) 2025-03-20 08:36:49 +01:00
Hans Mackowiak
b756bda988 to LF 2025-03-20 00:25:45 +01:00
Heitor Bittencourt
0e64a88005 fix: removed unused imports 2025-03-19 19:11:08 -04:00
Heitor Bittencourt
0d952a54bd feat/adventure: 4 new booster packs,spawn rate changed, removed colorless packs 2025-03-19 19:11:08 -04:00
Hans Mackowiak
d3ff7f3b61 Update parseAbilityCost (#7178)
cleanup and simplify
2025-03-19 22:48:57 +01:00
Renato Filipe Vidal Santos
0f9e7eca89 TDC: 2 cards 2025-03-19 19:18:24 +00:00
Paul Hammerton
207b786fcd oops 2025-03-19 14:01:26 -04:00
Paul Hammerton
0f6fa87da0 Alchemy Rebalancing for March 4, 2025 2025-03-19 14:01:26 -04:00
Paul Hammerton
995c1167dc Merge pull request #7201 from paulsnoops/edition-updates
Edition updates: SPG, TDC, TDM
2025-03-19 17:37:21 +00:00
Paul Hammerton
1ab1d9c002 Edition updates: SPG, TDC, TDM 2025-03-19 17:32:09 +00:00
Renato Filipe Vidal Santos
4b1a6a2f87 TDM: 2 cards (#7199) 2025-03-19 17:08:48 +00:00
Renato Filipe Vidal Santos
dacecd9006 TDM: 4 cards (#7197) 2025-03-19 16:16:19 +00:00
Fulgur14
dd5d75613e 10 TDM cards (All-Out Assault of them) (#7195) 2025-03-19 16:15:57 +00:00
Renato Filipe Vidal Santos
d59a316d8c TDM: 8 cards (#7189) 2025-03-19 16:44:06 +01:00
Fulgur14
9c4f855f71 10 TDM cards (#7196) 2025-03-19 16:41:01 +01:00
Fulgur14
90131b4a70 10 TDM cards (Taigam and pawns) (#7187) 2025-03-19 08:23:13 +01:00
Fulgur14
fb6725f2d7 Update zurgo_thunders_decree.txt (#7188) 2025-03-18 19:37:16 +00:00
tool4ever
475c57af55 Try fix AI not resetting context (#7186) 2025-03-18 19:06:49 +00:00
Renato Filipe Vidal Santos
6ae119a415 TDM: Zurgo, Thunder's Decree and mobilize support (#7185) 2025-03-18 18:51:43 +00:00
Renato Filipe Vidal Santos
8b5fe276e7 TDM: 3 cards (#7184) 2025-03-18 09:56:26 +01:00
Hans Mackowiak
5e4d5c262d ~ lf 2025-03-17 21:15:38 +01:00
Fulgur14
9b82f1ef1f Update united_battlefront.txt (#7183) 2025-03-17 19:56:22 +00:00
Paul Hammerton
321d2d7e33 Merge pull request #7182 from paulsnoops/edition-updates
Edition updates: FIN, SLD, TDM
2025-03-17 17:47:38 +00:00
Paul Hammerton
5b7cca95e1 Edition updates: FIN, SLD, TDM 2025-03-17 17:40:59 +00:00
Hans Mackowiak
2e0d53c6fe CantUntap Second Step (#7172)
* refactor more CantUntap Statics

* ~ no new hidden keywords

* ~ more CantBeActivated

* Update Card.java

* Refactor scripts

---------

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-03-17 17:48:39 +01:00
Fulgur14
9d4f6d2cbb United Battlefront (#7181) 2025-03-17 16:37:43 +00:00
tool4ever
9546b434e4 Update waltz_of_rage.txt 2025-03-17 17:01:40 +01:00
tool4ever
f230522657 Update contempt.txt (#7177) 2025-03-17 14:23:25 +01:00
tool4ever
a57a1f566a Refactor Telekinesis (#7174)
* Improve AI check
2025-03-17 12:06:03 +00:00
Renato Filipe Vidal Santos
8f8d6e6e30 FIN: Zell Dincht 2025-03-17 08:45:09 +00:00
tool4ever
3b8694483c Refactor Blinding Beam (#7171)
* Refactor Blinding Beam

* Refactor AI checking for Replacement while inactive

* Update stasis.txt

* Update sands_of_time.txt

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
Co-authored-by: Hans Mackowiak <hanmac@gmx.de>
2025-03-16 21:14:28 +01:00
Hans Mackowiak
4328a12967 Cant untap first step (#7162)
* CantHappen for UntapStep
2025-03-16 11:47:27 +01:00
tool4ever
fdc85b85c3 Cleanup "Cleanup" calls (#7169) 2025-03-16 10:04:10 +00:00
Chris H
bb2eed23b7 Fix Griselbrand overdrawing in one turn 2025-03-15 19:20:44 -04:00
Renato Filipe Vidal Santos
72e33146de YDFT: 14 cards (#7122) 2025-03-15 22:01:58 +00:00
HeitorBittenc
e371617938 fix: removed unused images, booster pack shops now respect the restricted cards and editions. (#7161) 2025-03-15 13:14:04 +00:00
tool4ever
b363db2bbd Fix North Star (#7163) 2025-03-15 13:03:51 +00:00
HeitorBittenc
37a5958750 Added 7 Card booster packs shops in Adventure Mode (#7141)
* Added 7 Card booster packs shops in Adventure Mode

* Update CHANGES.txt

* Fix: trailing space, extra comma and wrong color on shop fixed.

* Removed unused imports

---------

Co-authored-by: Heitor Bittencourt <heitorbite@outlook.com>
Co-authored-by: Agetian <stavdev@mail.ru>
2025-03-14 20:03:13 +03:00
Chris H
9091cfe3b0 Fix double mastesr 2022 basic lands 2025-03-14 11:36:57 -04:00
Chris H
e2614187ac Update brotherhood_vertibird.txt 2025-03-14 10:32:09 -04:00
Ayora29
5c69bf0470 Fix : Puzzle (Possibility Storm - Foundations #01). Opponent library is empty. (#7149) 2025-03-13 11:11:33 +01:00
tool4ever
383dc85166 Some cleanup (#7147) 2025-03-13 10:13:44 +01:00
Hans Mackowiak
7e47208888 Masako the humorless as Static 2025-03-13 08:37:47 +01:00
Hans Mackowiak
137d87c3df use StaticAbility for Topsy Turvy (#7143) 2025-03-12 09:24:53 +01:00
Paul Hammerton
fbff1fe10a Merge pull request #7144 from paulsnoops/edition-updates
Edition updates: CC2, FIC, PMEI, SLD, TDM
2025-03-11 08:31:42 +00:00
Paul Hammerton
3acd4490c5 Edition updates: CC2, FIC, PMEI, SLD, TDM 2025-03-11 08:28:25 +00:00
Hans Mackowiak
2952ed79f8 ~ lf 2025-03-11 07:07:08 +01:00
Fulgur14
cb23eda5a6 Stormplain Detainment [TDM] 2025-03-10 17:10:07 +00:00
Renato Filipe Vidal Santos
ced87c8aea FIC: Celes, Rune Knight 2025-03-10 15:49:40 +00:00
Jetz72
daf87e26ad Fix importing "Ice Tunnel"; Max Speed being flipped instead of transformed (#7138) 2025-03-10 14:31:35 +00:00
Chris H
0c30c4e32c Restrict conspiracy drafts in Adventure mode for now 2025-03-08 18:55:00 -05:00
Paul Hammerton
bf5f9f69ca Merge pull request #7133 from paulsnoops/edition-updates
Edition updates: PWCS
2025-03-08 20:50:08 +00:00
Paul Hammerton
b4828d3b4d Edition updates: PWCS 2025-03-08 20:35:44 +00:00
Renato Filipe Vidal Santos
617df8e07c Fixing Chimeric Mass and Svogthos (#7130) 2025-03-08 08:15:03 +00:00
Simisays
deaba89f46 jace update 2025-03-07 21:23:23 -05:00
Hans Mackowiak
d5b13d56cc Update jackdaw.txt
Closes #7127
2025-03-07 09:26:32 +01:00
Chris H
f893c7ddf8 Balance some item costs 2025-03-06 12:14:00 -05:00
Paul Hammerton
c6cf450ac4 Merge pull request #7121 from paulsnoops/edition-updates
Edition updates: TDM
2025-03-04 18:40:10 +00:00
Paul Hammerton
fa123023c7 Edition updates: TDM 2025-03-04 18:24:38 +00:00
Fulgur14
0b285fa045 Rally the Monastery (#7120) 2025-03-04 17:03:31 +00:00
Paul Hammerton
fe6c4243ee Merge pull request #7119 from paulsnoops/ydft-formats
Add YDFT to formats
2025-03-04 14:06:36 +00:00
Paul Hammerton
1a2c18d25e Add YDFT to formats 2025-03-04 14:03:04 +00:00
Renato Filipe Vidal Santos
12e6de4697 Highway Reaver fix (#7118) 2025-03-04 14:58:02 +01:00
Paul Hammerton
f2b72d4234 Merge pull request #7090 from dracontes/rb-ydft-3
YDFT: 12 cards
2025-03-04 11:22:35 +00:00
Renato Filipe Vidal Santos
25a84e9aee Update venom_deadly_devourer.txt (#7117) 2025-03-04 11:47:54 +01:00
Hans Mackowiak
488171b02b Dream counters rework (#7116)
* Dream Counters: moved to extra getCounterMax function

* Update Rasputin Dreamweaver State-Based-Action with a StaticAbility
2025-03-04 11:33:57 +01:00
Renato Filipe Vidal Santos
eb22a449f4 Update underworld_sentinel.txt 2025-03-04 09:25:14 +00:00
Renato Filipe Vidal Santos
c21c043f5f Add files via upload 2025-03-04 06:05:23 +00:00
Simisays
94808ea73e Update vampirecastle_4.tmx 2025-03-03 18:57:32 -05:00
Simisays
7fe486cba6 Update zedruu.tmx 2025-03-03 18:57:32 -05:00
Simisays
a3517e260c update 2025-03-03 18:57:32 -05:00
Paul Hammerton
377f1fad41 Update MagicFest 2025.txt 2025-03-03 13:30:44 -05:00
Chris H
17448f99c9 Update MagicFest 2025.txt 2025-03-03 13:30:44 -05:00
Hans Mackowiak
8c83886c2e Update legions_to_ashes.txt
fix missing OppCtrl
2025-03-03 16:10:56 +01:00
Renato Filipe Vidal Santos
9cef38af60 SPE: Sensational Spider-Man (#7112) 2025-03-03 15:54:00 +01:00
tool4ever
288ecc0d72 Support Sensational Spider-Man (#7110) 2025-03-03 15:52:35 +01:00
Renato Filipe Vidal Santos
bb514f6d08 Update fallaji_antiquarian.txt 2025-03-03 10:06:50 +00:00
Chris H
ecb21abb9a Delay rendering the next dialog by a short bit, to allow for the TouchUp keypress to clear 2025-03-02 21:04:07 -05:00
Chris H
9445093d68 Fix Wanderlust from breaking other quests 2025-03-02 21:04:07 -05:00
Renato Filipe Vidal Santos
8f3f83051b SPE: 5 cards (#7107) 2025-03-02 21:49:24 +00:00
Renato Filipe Vidal Santos
5750892edf Add files via upload 2025-03-02 17:47:48 +00:00
Renato Filipe Vidal Santos
db74b2b70b Update shops.json 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
a5c036be05 Update quests.json 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
b60ee73ce5 Add files via upload 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
b743f1cbc5 Update golem.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
0f0bb56f7d Update goblins.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
8b593f8356 Update bluewizard_apprentice_2.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
79ac73fb15 Update banditarcher_damage.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
da0bb4c0ce Update lorthos.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
9d7617add9 Update emrakul.dck 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
f38ed39c87 Update shops.json 2025-03-02 12:35:37 -05:00
Renato Filipe Vidal Santos
8ff5f45449 Update ChooseCardNameEffect.java 2025-03-02 17:19:14 +00:00
Hans Mackowiak
0447299e4f StaticAbility InfectDamage for Phyrexian Unlife 2025-03-02 11:42:37 +01:00
Paul Hammerton
b797317f1a Merge pull request #7106 from paulsnoops/edition-updates
Edition updates: PLG25, PSLDSC, SLD, SPE, YDFT
2025-03-01 22:27:41 +00:00
Paul Hammerton
388f334fd3 Edition updates: PLG25, PSLDSC, SLD, SPE, YDFT 2025-03-01 22:22:15 +00:00
Chris H
e47f623b0a Update argent_dais.txt (#7103) 2025-03-01 18:28:00 +00:00
Renato Filipe Vidal Santos
93e71bded8 Add files via upload 2025-03-01 16:09:56 +00:00
Renato Filipe Vidal Santos
8a93283398 Add files via upload 2025-03-01 11:43:07 +00:00
Renato Filipe Vidal Santos
aafd9b8f49 Update fear_of_change.txt 2025-03-01 08:52:38 +00:00
Renato Filipe Vidal Santos
32fec183d5 Add files via upload 2025-03-01 08:51:56 +00:00
Agetian
d1751262df Make canBlock check if the blocker is a creature. (#7098)
- TokenAi also checks if the spawned token is a creature before running checks.
2025-02-28 19:09:37 +00:00
tool4ever
d05523360d Fix missing SpellDescription (#7099) 2025-02-28 19:08:53 +00:00
Hans Mackowiak
a32f9a3c1c Update a-uurg_spawn_of_turg.txt 2025-02-28 14:38:32 +01:00
Hans Mackowiak
0468247c5a Update uurg_spawn_of_turg.txt 2025-02-28 14:38:07 +01:00
Hans Mackowiak
d02dd67016 Hidden and Double Agenda better as Keyword (#7093)
* Hidden and Double Agenda better as Keyword
2025-02-28 10:22:13 +01:00
tool4ever
27d5766abb Update aetherflux_conduit.txt 2025-02-28 09:13:02 +01:00
Renato Filipe Vidal Santos
be88c63414 Update decoy_gambit.txt (#7095) 2025-02-28 09:08:36 +01:00
Renato Filipe Vidal Santos
c0d965857a Add files via upload 2025-02-28 07:42:50 +00:00
Renato Filipe Vidal Santos
e1763c45af Update quicksilver_lapidary.txt 2025-02-28 04:28:43 +00:00
Renato Filipe Vidal Santos
177f7f64ac Update euru_acorn_scrounger.txt 2025-02-28 04:28:04 +00:00
Renato Filipe Vidal Santos
a2096aa753 Update sala_deck_boss.txt 2025-02-27 21:29:40 +00:00
Renato Filipe Vidal Santos
bc9fac1da6 Add files via upload 2025-02-27 21:23:18 +00:00
Renato Filipe Vidal Santos
e0dcf24c90 Update banquet_guests.txt 2025-02-27 19:36:39 +00:00
Paul Hammerton
db5eb095aa Merge pull request #7094 from Agetian/lda-update-dft
Update LDA deck generation information for Aetherdrift
2025-02-27 18:11:39 +00:00
marthinwurer
16b87f20ca Delegate to piles all the time 2025-02-27 11:07:44 -05:00
Agetian
00b82fe9f9 - Update LDA information (Aetherdrift) 2025-02-27 14:06:23 +03:00
Renato Filipe Vidal Santos
6af0ad100e Update marina_vendrell.txt 2025-02-27 06:17:25 +01:00
Renato Filipe Vidal Santos
f14fda5d1b Update ForgeScript.java 2025-02-27 02:13:01 +00:00
Renato Filipe Vidal Santos
1ef027e7e2 Add files via upload 2025-02-27 02:11:08 +00:00
Renato Filipe Vidal Santos
61aaba268f Update agent_of_masks.txt 2025-02-26 19:34:40 +00:00
Renato Filipe Vidal Santos
b041eee060 Update a-blood_artist.txt 2025-02-26 19:33:52 +00:00
Renato Filipe Vidal Santos
8ed65b95f2 Update blood_artist.txt 2025-02-26 19:32:41 +00:00
Renato Filipe Vidal Santos
3fe53f601c Add files via upload 2025-02-26 19:31:38 +00:00
Paul Hammerton
5ce0374426 Merge pull request #7089 from paulsnoops/edition-updates
Edition updates: YDFT
2025-02-26 19:29:38 +00:00
Paul Hammerton
e4aba70090 Edition updates: YDFT 2025-02-26 19:21:22 +00:00
Renato Filipe Vidal Santos
b17824c20a YDFT: Support Skyforge 2025-02-26 18:16:20 +01:00
Chris H
976dd18fa2 Update negan_the_cold_blooded.txt (#7085) 2025-02-26 18:15:58 +01:00
Renato Filipe Vidal Santos
56bfcec656 Incidental cleanup pass #4 (#7084) 2025-02-26 18:15:43 +01:00
Chris H
f935706d22 Disable upload workflows to ftp forge 2025-02-26 08:52:36 -05:00
Renato Filipe Vidal Santos
e2322ee7ef YDFT: 3 cards (#7077) 2025-02-26 09:53:15 +01:00
Chris H
d553a7cac4 Update stairs_to_infinity.txt (#7082) 2025-02-26 06:00:24 +01:00
tool4ever
f75b2ad9ee Update neriv_crackling_vanguard.txt 2025-02-25 19:42:22 +00:00
tool4ever
333b25eeaf Update yannik_scavenging_sentinel.txt 2025-02-25 19:09:32 +00:00
Hans Mackowiak
0db70261f9 Update TypeLists.txt
Add missing Glimmer
2025-02-25 19:29:53 +01:00
tool4ever
504db590db Fix All in Good Time (#7081)
* Fix for Ketramose
2025-02-25 18:25:35 +00:00
Agetian
650b667148 - Add an AI hint for Arid Archway. (#7080) 2025-02-25 17:17:21 +03:00
Paul Hammerton
4afdd0c264 Merge pull request #7079 from paulsnoops/edition-updates
Edition updates
2025-02-25 10:22:17 +00:00
Paul Hammerton
2816bdef85 Edition updates: SLD, TDC, YDFT 2025-02-25 10:18:25 +00:00
Paul Hammerton
855fe70281 Edition updates: SLD, TDC, YDFT 2025-02-25 10:17:04 +00:00
Fulgur14
e4071a4f4e Neriv and Elsha (TDC) (#7078) 2025-02-25 11:06:36 +01:00
Hans Mackowiak
18ee17f7c8 Update TypeLists.txt
Fix missing Balloon type
2025-02-25 09:26:47 +01:00
Chris H
f84f694351 Reroute card images to scryfall.
Don't try to redownload if it fails during a session
2025-02-24 22:50:14 -05:00
tool4ever
0a15a0352d Fix battles attacking/blocking (#7075)
* Add battle SBA

---------

Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.59>
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-24 16:03:58 +01:00
tool4ever
0e46d436de Update mimeoplasm_revered_one.txt 2025-02-24 08:00:48 +01:00
Hans Mackowiak
a16b4ffe75 CostBehold: add Special Reveal Cost (#7072)
* CostBehold: add Special Reveal Cost

----

Co-authored-by: tool4ever <therealtoolkit@hotmail.com>
2025-02-23 16:52:24 +01:00
tool4ever
608d4c5bda Don't update view of LKI instead of real Card (#7070)
Co-authored-by: tool4EvEr <tool4EvEr@192.168.0.60>
2025-02-23 15:18:40 +03:00
Paul Hammerton
dbb8d8c93a Merge pull request #7071 from paulsnoops/update-tdc
Edition updates: TDC
2025-02-23 10:36:42 +00:00
Paul Hammerton
e30f9a6cb1 Edition updates: TDC 2025-02-23 10:22:21 +00:00
Renato Filipe Vidal Santos
bd4f5a2aa4 TDC: Betor, Ancestor's Voice (#7069) 2025-02-23 09:43:54 +00:00
Renato Filipe Vidal Santos
2d352b110b Negation cleanup: IsUnsolved (#7068) 2025-02-22 22:04:05 +00:00
Renato Filipe Vidal Santos
2802c61abd Negation cleanup: notAttackedThisTurn (#7066) 2025-02-22 21:37:02 +00:00
Paul Hammerton
62c27f9142 Replace net deck links 2025-02-22 15:48:16 -05:00
Renato Filipe Vidal Santos
c358f4e71f Negation cleanup: IsNotCommander (#7064) 2025-02-22 20:38:15 +00:00
Chris H
a05ecbc810 Update teval_the_balanced_scale.txt (#7065) 2025-02-22 20:34:55 +00:00
tool4ever
6b299693ca Finish nonToken cleanup (#7063) 2025-02-22 19:26:48 +00:00
Renato Filipe Vidal Santos
bb3413c1e5 Negation cleanup: nonToken, part 3 (#6952) 2025-02-22 17:44:28 +00:00
Renato Filipe Vidal Santos
854267d521 Negation cleanup: nonToken, part 2 (#6951) 2025-02-22 17:42:28 +00:00
Renato Filipe Vidal Santos
c4e05a5d9b Negation cleanup: nonToken, part 1 (#6950) 2025-02-22 17:42:17 +00:00
Renato Filipe Vidal Santos
27b61a79c8 Negation cleanup: nonToken, part 4 (#6953) 2025-02-22 17:42:03 +00:00
Chris H
70183bcc85 Update snapshot-both-pc-android.yml 2025-02-22 11:19:33 -05:00
Hans Mackowiak
94da663287 ~ lf 2025-02-22 15:00:59 +01:00
Paul Hammerton
3c7c1cc4c7 Merge pull request #7060 from paulsnoops/edition-updates
Edition updates: SLD, TDC, TDM
2025-02-22 10:39:27 +00:00
Paul Hammerton
3b773da60d Edition updates: SLD, TDC, TDM 2025-02-22 10:34:19 +00:00
Renato Filipe Vidal Santos
afee15cf44 TDM: 5 cards (#7058) 2025-02-22 10:17:39 +00:00
Fulgur14
a424aa65df 2 TDM and 1 TDC card (#7056) 2025-02-22 09:03:40 +00:00
tool4EvEr
fa93a7dfdd Fix Well-Laid Plans 2025-02-21 10:46:14 -05:00
Renato Filipe Vidal Santos
446f60b331 Add files via upload 2025-02-21 15:22:33 +01:00
tool4ever
e136368ce3 Update narset_jeskai_waymaster.txt 2025-02-21 10:33:06 +01:00
Fulgur14
5f1b54860c Narset, Jeskai Waymaster (TDM) (#7052) 2025-02-21 11:59:24 +03:00
Chris H
23eb008d5f Trigger edition change if playing 10E draft on mobile 2025-02-20 20:13:21 -05:00
Chris H
40882c20d6 Send URL to github snapshots 2025-02-20 20:13:04 -05:00
8352 changed files with 282088 additions and 63092 deletions

View File

@@ -2,10 +2,21 @@ name: Publish Desktop Forge
on:
workflow_dispatch:
inputs:
debug_enabled:
type: boolean
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
release_android:
type: boolean
description: 'Also try to release android build'
required: false
default: false
jobs:
build:
if: github.repository_owner == 'Card-Forge'
runs-on: ubuntu-latest
permissions:
contents: write
@@ -32,10 +43,132 @@ jobs:
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions"
- name: Build/Install/Publish to GitHub Packages Apache Maven
- name: Install old maven (3.8.1)
run: |
curl -o apache-maven-3.8.1-bin.tar.gz https://archive.apache.org/dist/maven/maven-3/3.8.1/binaries/apache-maven-3.8.1-bin.tar.gz
tar xf apache-maven-3.8.1-bin.tar.gz
export PATH=$PWD/apache-maven-3.8.1/bin:$PATH
export MAVEN_HOME=$PWD/apache-maven-3.8.1
mvn --version
- name: Setup tmate session
uses: mxschmitt/action-tmate@v3
if: ${{ github.event_name == 'workflow_dispatch' && inputs.debug_enabled }}
- name: Setup android requirements
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
run: |
JAVA_HOME=${JAVA_HOME_17_X64} ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=$ANDROID_SDK_ROOT --install "build-tools;35.0.0" "platform-tools" "platforms;android-35"
cd forge-gui-android
echo "${{ secrets.FORGE_KEYSTORE }}" > forge.keystore.asc
gpg -d --passphrase "${{ secrets.FORGE_KEYSTORE_PASSPHRASE }}" --batch forge.keystore.asc > forge.keystore
cd -
mkdir -p ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
cd ~/.m2/repository/com/simpligility/maven/plugins/android-maven-plugin/4.6.2
curl -L -o android-maven-plugin-4.6.2.jar https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.jar
curl -L -o android-maven-plugin-4.6.2.pom https://github.com/Card-Forge/android-maven-plugin/releases/download/4.6.2/android-maven-plugin-4.6.2.pom
cd -
mvn install -Dmaven.test.skip=true
mvn dependency:tree
- name: Build/Install/Publish Desktop to GitHub Packages Apache Maven
if: ${{ github.event_name == 'workflow_dispatch' && !inputs.release_android }}
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
mvn -U -B clean -P windows-linux install release:clean release:prepare release:perform -T 1C -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }}
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build only desktop and only try to move desktop files
mvn -U -B clean -P windows-linux install -DskipTests -Dskip.flatten=true -e -T 1C release:clean release:prepare release:perform
mkdir izpack
# move bz2 and jar from work dir to izpack dir
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
# move desktop build.txt and version.txt to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
cd izpack
ls
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Build/Install/Publish Desktop+Android to GitHub Packages Apache Maven
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_android }}
run: |
export DISPLAY=":1"
Xvfb :1 -screen 0 800x600x8 &
export _JAVA_OPTIONS="-Xmx2g"
d=$(date +%m.%d)
# build both desktop and android
mvn -U -B clean -P windows-linux,android-release-build install -e -Dcardforge-repo.username=${{ secrets.FTP_USERNAME }} -Dcardforge-repo.password=${{ secrets.FTP_PASSWORD }} -Dandroid.sdk.path=/usr/local/lib/android/sdk -Dandroid.buildToolsVersion=35.0.0
mkdir izpack
# move bz2 and jar from work dir to izpack dir
mv /home/runner/work/forge/forge/forge-installer/*/*.{bz2,jar} izpack/
# move desktop build.txt and version.txt to izpack
mv /home/runner/work/forge/forge/forge-gui-desktop/target/classes/*.txt izpack/
# move android apk and assets.zip
mv /home/runner/work/forge/forge/forge-gui-android/target/*-signed-aligned.apk izpack/
mv /home/runner/work/forge/forge/forge-gui-android/target/assets.zip izpack/
cd izpack
ls
echo "GIT_TAG=`echo $(git describe --tags --abbrev=0)`" >> $GITHUB_ENV
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Upload snapshot to GitHub Prerelease
uses: ncipollo/release-action@v1
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
name: Release ${{ env.GIT_TAG }}
tag: ${{ env.GIT_TAG }}
artifacts: izpack/*
allowUpdates: true
removeArtifacts: true
makeLatest: true
- name: 🔧 Install XML tools
run: sudo apt-get install -y libxml2-utils
- name: 🔼 Bump versionCode in root POM
id: bump_version
run: |
cd /home/runner/work/forge/forge/
current_version=$(xmllint --xpath "//*[local-name()='versionCode']/text()" pom.xml)
echo "Current versionCode: $current_version"
IFS='.' read -r major minor patch <<< "${current_version}"
new_patch=$(printf "%02d" $((10#$patch + 1)))
new_version="${major}.${minor}.${new_patch}"
sed -i -E "s|<versionCode>.*</versionCode>|<versionCode>${new_version}</versionCode>|" pom.xml
echo "version_code=${new_version}" >> $GITHUB_OUTPUT
- name: ♻️ Restore {revision} in child POMs
run: |
find . -name pom.xml ! -path "./pom.xml" | while read -r pom; do
sed -i -E 's|<version>2\.0+\.[0-9]+(-SNAPSHOT)?</version>|<version>${revision}</version>|' "$pom"
done
- name: 💾 Commit restored {revision}
run: |
# Add only pom.xml files
find . -name pom.xml -exec git add {} \;
# Commit if there are changes
if git diff --cached --quiet; then
echo "No pom.xml changes to commit."
else
git commit -m "Restore POM files for preparation of next release" || echo "No changes to commit"
git push
fi
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Release Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -122,3 +122,11 @@ jobs:
prerelease: true
artifacts: izpack/*
allowUpdates: true
removeArtifacts: true
- name: Send failure notification to Discord
if: failure() # This step runs only if the job fails
run: |
curl -X POST -H "Content-Type: application/json" \
-d "{\"content\": \"🔴 Snapshot Build Failed in branch: \`${{ github.ref_name }}\` by \`${{ github.actor }}\`.\nCheck logs: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"}" \
${{ secrets.DISCORD_AUTOMATION_WEBHOOK }}

View File

@@ -13,10 +13,6 @@ on:
# description: 'Upload the completed Android package'
# required: false
# default: true
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '00 19 * * *'
jobs:
build:

View File

@@ -8,9 +8,6 @@ on:
description: 'Run the build with tmate debugging enabled (https://github.com/marketplace/actions/debugging-with-tmate)'
required: false
default: false
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '30 18 * * *'
jobs:
build:

View File

@@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
java: [ '17' ]
java: ['17', '21']
name: Test with Java ${{ matrix.Java }}
steps:
- uses: actions/checkout@v3

7
.gitignore vendored
View File

@@ -66,6 +66,9 @@ forge-gui-mobile-dev/testAssets
forge-gui/res/cardsfolder/*.bat
# Generated changelog file
forge-gui/release-files/CHANGES.txt
forge-gui/res/PerSetTrackingResults
forge-gui/res/decks
forge-gui/res/layouts
@@ -87,3 +90,7 @@ forge-gui/tools/PerSetTrackingResults
*.tiled-session
/forge-gui/res/adventure/*.tiled-project
/forge-gui/res/adventure/*.tiled-session
# Ignore python temporaries
__pycache__
*.pyc

View File

@@ -1,33 +0,0 @@
Summary
(Summarize the bug encountered concisely)
Steps to reproduce
(How one can reproduce the issue - this is very important. Specific cards and specific actions especially)
Which version of Forge are you on (Release, Snapshot? Desktop, Android?)
What is the current bug behavior?
(What actually happens)
What is the expected correct behavior?
(What you should see instead)
Relevant logs and/or screenshots
(Paste/Attach your game.log from the crash - please use code blocks (```)) Also, provide screenshots of the current state.
Possible fixes
(If you can, link to the line of code that might be responsible for the problem)
/label ~needs-investigation

View File

@@ -1,15 +0,0 @@
Summary
(Summarize the feature you wish concisely)
Example screenshots
(If this is a UI change, please provide an example screenshot of how this feature might work)
Feature type
(Where in Forge does this belong? e.g. Quest Mode, Deck Editor, Limited, Constructed, etc.)
/label ~feature request

View File

@@ -6,7 +6,7 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
## Requirements / Tools
- you favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- your favourite Java IDE (IntelliJ, Eclipse, VSCodium, Emacs, Vi...)
- Java JDK 17 or later
- Git
- Git client (optional)
@@ -28,7 +28,6 @@ Dev instructions here: [Getting Started](https://github.com/Card-Forge/forge/wik
IntelliJ is the recommended IDE for Forge development. Quick start guide for [setting up the Forge project within IntelliJ](https://github.com/Card-Forge/forge/wiki/IntelliJ-setup).
## Eclipse
Eclipse includes Maven integration so a separate install is not necessary. For other IDEs, your mileage may vary.
@@ -124,10 +123,11 @@ TBD
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...
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.
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
@@ -158,13 +158,19 @@ The platform-specific projects are:
#### forge-ai
The forge-ai project contains the computer opponent logic for gameplay. It includes decision-making algorithms for specific abilities, cards and turn phases.
#### forge-core
The forge-core project contains the core game engine, card mechanics, rules engine, and fundamental game logic. It includes the implementation of Magic: The Gathering rules, card interactions, and the game state management system.
#### forge-game
The forge-game project handles the game session management, player interactions, and game flow control. It includes implementations for multiplayer support, game modes, matchmaking, and game state persistence. This module bridges the core game engine with the user interface and networking components.
#### forge-gui
The forge-gui project includes the scripting resource definitions in the res/ path.
The forge-gui project contains the user interface components and rendering logic for the game. It includes the main game window, card displays, player interactions, and the scripting resource definitions in the res/ path.
#### forge-gui-android

View File

@@ -26,13 +26,14 @@ Join the **Forge community** on [Discord](https://discord.gg/HcPJNyD66a)!
### 📥 Desktop Installation
1. **Latest Releases:** Download the latest version [here](https://github.com/Card-Forge/forge/releases/latest).
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/).
2. **Snapshot Build:** For the latest development version, grab the `forge-gui-desktop` tarball from our [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots).
- **Tip:** Extract to a new folder to prevent version conflicts.
3. **User Data Management:** Previous players data is preserved during upgrades.
4. **Java Requirement:** Ensure you have **Java 17 or later** installed.
### 📱 Android Installation
- Download the **APK** from the [Snapshot Build](https://downloads.cardforge.org/dailysnapshots/). On the first launch, Forge will automatically download all necessary assets.
- _(Note: **Android 11** is the minimum requirements with at least **6GB RAM** to run smoothly. You need to enable **"Install unknown apps"** for Forge to initialize and update itself)_
- Download the **APK** from the [Snapshot Build](https://github.com/Card-Forge/forge/releases/tag/daily-snapshots). On the first launch, Forge will automatically download all necessary assets.
---
@@ -46,11 +47,13 @@ Embark on a thrilling single-player journey where you can:
- Challenge diverse AI opponents.
- Collect cards and items to boost your abilities.
![Adventure Mode](https://downloads.cardforge.org/images/site/adventure-mode.png "Adventure Mode")
<img width="1282" height="752" alt="Shandalar World" src="https://github.com/user-attachments/assets/9af31471-d688-442f-9418-9807d8635b72" />
### 🔍 Quest Modes
Engage in focused gameplay without the overworld exploration—perfect for quick sessions!
<img width="1282" height="752" alt="Quest Duels" src="https://github.com/user-attachments/assets/b9613b1c-e8c3-4320-8044-6922c519aad4" />
### 🤖 AI Formats
Test your skills against AI in multiple formats:
- **Sealed**
@@ -60,6 +63,8 @@ Test your skills against AI in multiple formats:
For comprehensive gameplay instructions, visit our [Gameplay Guide](https://github.com/Card-Forge/forge/wiki/Gameplay-Guide).
<img width="1282" height="752" alt="Sealed" src="https://github.com/user-attachments/assets/ae603dbd-4421-4753-a333-87cb0a28d772" />
---
## 💬 Support & Community

View File

@@ -15,7 +15,7 @@ public class Main {
public static void main(String[] args) {
GuiBase.setInterface(new GuiMobile(Files.exists(Paths.get("./res"))?"./":"../forge-gui/"));
GuiBase.setDeviceInfo("", "", 0, 0);
GuiBase.setDeviceInfo(null, 0, 0);
new EditorMainWindow(Config.instance());
}
}

View File

@@ -0,0 +1,9 @@
package forge.ai;
public record AiAbilityDecision(int rating, AiPlayDecision decision) {
private static int MIN_RATING = 30;
public boolean willingToPlay() {
return rating > MIN_RATING && decision.willingToPlay();
}
}

View File

@@ -37,6 +37,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
@@ -144,13 +145,15 @@ public class AiAttackController {
sa.setActivatingPlayer(defender);
if (sa.isCrew() && !ComputerUtilCost.checkTapTypeCost(defender, sa.getPayCosts(), c, sa, tappedDefenders)) {
continue;
} else if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
}
if (!ComputerUtilCost.canPayCost(sa, defender, false) || !sa.getRestrictions().checkOtherRestrictions(c, sa, defender)) {
continue;
}
Card animatedCopy = AnimateAi.becomeAnimated(c, sa);
if (animatedCopy.isCreature()) {
// TODO imprecise, only works 100% for colorless mana
int saCMC = sa.getPayCosts() != null && sa.getPayCosts().hasManaCost() ?
sa.getPayCosts().getTotalMana().getCMC() : 0; // FIXME: imprecise, only works 100% for colorless mana
sa.getPayCosts().getTotalMana().getCMC() : 0;
if (totalMana - manaReserved >= saCMC) {
manaReserved += saCMC;
defenders.add(animatedCopy);
@@ -1587,7 +1590,7 @@ public class AiAttackController {
// but there are no creatures it can target, no need to exert with it
boolean missTarget = false;
for (StaticAbility st : c.getStaticAbilities()) {
if (!"OptionalAttackCost".equals(st.getParam("Mode"))) {
if (!st.checkMode(StaticAbilityMode.OptionalAttackCost)) {
continue;
}
SpellAbility sa = st.getPayingTrigSA();

View File

@@ -59,7 +59,6 @@ public class AiCardMemory {
ATTACHED_THIS_TURN, // These equipments were attached to something already this turn
ANIMATED_THIS_TURN, // These cards had their AF Animate effect activated this turn
BOUNCED_THIS_TURN, // These cards were bounced this turn
ACTIVATED_THIS_TURN, // These cards had their ability activated this turn
CHOSEN_FOG_EFFECT, // These cards are marked as the Fog-like effect the AI is planning to cast this turn
MARKED_TO_AVOID_REENTRY, // These cards may cause a stack smash when processed recursively, and are thus marked to avoid a crash
PAYS_TAP_COST, // These cards will be tapped as part of a cost and cannot be chosen in another part

View File

@@ -54,6 +54,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.*;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityDisableTriggers;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustTarget;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -65,10 +66,10 @@ import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import java.util.*;
import java.util.concurrent.FutureTask;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
@@ -292,7 +293,7 @@ public class AiController {
}
// can't fetch partner isn't problematic
if (tr.getKeyword() != null && tr.getKeyword().getOriginal().startsWith("Partner")) {
if (tr.isKeyword(Keyword.PARTNER)) {
continue;
}
@@ -480,7 +481,7 @@ public class AiController {
if (lands.size() >= Math.max(maxCmcInHand, 6)) {
// don't play MDFC land if other side is spell and enough lands are available
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Modal).getType().isLand())) {
if (!c.isLand() || (c.isModal() && !c.getState(CardStateName.Backside).getType().isLand())) {
return false;
}
@@ -689,7 +690,6 @@ public class AiController {
}
}
// TODO handle fetchlands and what they can fetch for
// determine new color pips
int[] card_counts = new int[6]; // in WUBRGC order
@@ -909,56 +909,14 @@ public class AiController {
}
}
int oldCMC = -1;
boolean xCost = sa.costHasX() || host.hasKeyword(Keyword.STRIVE) || sa.getApi() == ApiType.Charm;
if (!xCost) {
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// for most costs, it's OK to check if they can be paid early in order to avoid running a heavy API check
// when the AI won't even be able to play the spell in the first place (even if it could afford it)
return AiPlayDecision.CantAfford;
}
// TODO check for Reduce too, e.g. Battlefield Thaumaturge could make it castable
if (!sa.getAllTargetChoices().isEmpty()) {
oldCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
}
}
AiPlayDecision canPlay = canPlaySa(sa); // this is the "heaviest" check, which also sets up targets, defines X, etc.
// this is the "heaviest" check, which also sets up targets, defines X, etc.
AiPlayDecision canPlay = canPlaySa(sa);
if (canPlay != AiPlayDecision.WillPlay) {
return canPlay;
}
// Account for possible Ward after the spell is fully targeted
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
if (!sa.isSpell() || sa.isCounterableBy(null)) {
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(host.getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
if (wardCost.hasManaCost()) {
xCost |= wardCost.getTotalMana().getCMC() > 0;
}
SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(player, sa, wardCost, host)) {
return AiPlayDecision.CostNotAcceptable;
}
}
}
}
}
// check if some target raised cost
if (!xCost && oldCMC > -1) {
int finalCMC = CostAdjustment.adjust(sa.getPayCosts(), sa, false).getTotalMana().getCMC();
if (finalCMC > oldCMC) {
xCost = true;
}
}
if (xCost && !ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
if (!ComputerUtilCost.canPayCost(sa, player, sa.isTrigger())) {
// for dependent costs with X, e.g. Repeal, which require a valid target to be specified before a decision can be made
// on whether the cost can be paid, this can only be checked late after canPlaySa has been run (or the AI will misplay)
return AiPlayDecision.CantAfford;
@@ -971,8 +929,6 @@ public class AiController {
return AiPlayDecision.CantAfford;
}
// if we got here, looks like we can play the final cost and we could properly set up and target the API and
// are willing to play the SA
return AiPlayDecision.WillPlay;
}
@@ -1015,7 +971,7 @@ public class AiController {
Sentry.setExtra("Card", card.getName());
Sentry.setExtra("SA", sa.toString());
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayAIWithSubs(player, sa);
boolean canPlay = SpellApiToAi.Converter.get(sa).canPlayWithSubs(player, sa).willingToPlay();
// remove added extra
Sentry.removeExtra("Card");
@@ -1128,7 +1084,7 @@ public class AiController {
// Memory Crystal-like effects need special handling
for (Card c : game.getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility s : c.getStaticAbilities()) {
if ("ReduceCost".equals(s.getParam("Mode"))
if (s.checkMode(StaticAbilityMode.ReduceCost)
&& "Spell.Buyback".equals(s.getParam("ValidSpell"))) {
neededMana -= AbilityUtils.calculateAmount(c, s.getParam("Amount"), s);
}
@@ -1393,9 +1349,9 @@ public class AiController {
if (spell instanceof SpellApiBased) {
boolean chance = false;
if (withoutPayingManaCost) {
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory);
chance = SpellApiToAi.Converter.get(spell).doTriggerNoCostWithSubs(player, spell, mandatory).willingToPlay();
} else {
chance = SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
chance = SpellApiToAi.Converter.get(spell).doTrigger(player, spell, mandatory);
}
if (!chance) {
return AiPlayDecision.TargetingFailed;
@@ -1708,7 +1664,7 @@ public class AiController {
Sentry.captureMessage(ex.getMessage() + "\nAssertionError [verifyTransitivity]: " + assertex);
}
CompletableFuture<SpellAbility> future = CompletableFuture.supplyAsync(() -> {
FutureTask<SpellAbility> future = new FutureTask<>(() -> {
//avoid ComputerUtil.aiLifeInDanger in loops as it slows down a lot.. call this outside loops will generally be fast...
boolean isLifeInDanger = useLivingEnd && ComputerUtil.aiLifeInDanger(player, true, 0);
for (final SpellAbility sa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, player)) {
@@ -1786,13 +1742,18 @@ public class AiController {
return null;
});
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
Thread t = new Thread(future);
t.start();
try {
if (game.AI_CAN_USE_TIMEOUT)
return future.completeOnTimeout(null, game.getAITimeout(), TimeUnit.SECONDS).get();
else
// instead of computing all available concurrently just add a simple timeout depending on the user prefs
return future.get(game.getAITimeout(), TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
try {
t.stop();
} catch (UnsupportedOperationException ex) {
// Android and Java 20 dropped support to stop so sadly thread will keep running
future.cancel(true);
}
return null;
}
}
@@ -1803,9 +1764,9 @@ public class AiController {
for (int i = 0; i < numToExile; i++) {
Card chosen = null;
for (final Card c : grave) { // Exile noncreatures first in
// case we can revive. Might wanna do some additional
// checking here for Flashback and the like.
for (final Card c : grave) {
// Exile noncreatures first in case we can revive
// Might wanna do some additional checking here for Flashback and the like
if (!c.isCreature()) {
chosen = c;
break;
@@ -1826,12 +1787,12 @@ public class AiController {
return toExile;
}
public boolean doTrigger(SpellAbility spell, boolean mandatory) {
if (spell instanceof WrappedAbility)
return doTrigger(((WrappedAbility) spell).getWrappedAbility(), mandatory);
if (spell.getApi() != null)
return SpellApiToAi.Converter.get(spell).doTriggerAI(player, spell, mandatory);
if (spell.getPayCosts() == Cost.Zero && !spell.usesTargeting()) {
public boolean doTrigger(SpellAbility sa, boolean mandatory) {
if (sa instanceof WrappedAbility)
return doTrigger(((WrappedAbility) sa).getWrappedAbility(), mandatory);
if (sa.getApi() != null)
return SpellApiToAi.Converter.get(sa).doTrigger(player, sa, mandatory);
if (sa.getPayCosts() == Cost.Zero && !sa.usesTargeting()) {
// For non-converted triggers (such as Cumulative Upkeep) that don't have costs or targets to worry about
return true;
}
@@ -2368,7 +2329,7 @@ public class AiController {
// TODO move to more common place
public static <T extends TriggerReplacementBase> List<T> filterList(List<T> input, Function<SpellAbility, Object> pred, Object value) {
return filterList(input, trb -> pred.apply(trb.ensureAbility()) == value);
return filterList(input, trb -> trb.ensureAbility() != null && pred.apply(trb.ensureAbility()) == value);
}
public static List<SpellAbility> filterListByApi(List<SpellAbility> input, ApiType type) {

View File

@@ -46,6 +46,14 @@ public class AiCostDecision extends CostDecisionMakerBase {
return PaymentDecision.number(c);
}
@Override
public PaymentDecision visit(CostBehold cost) {
final String type = cost.getType();
CardCollectionView hand = player.getCardsIn(cost.getRevealFrom());
hand = CardLists.getValidCards(hand, type.split(";"), player, source, ability);
return hand.isEmpty() ? null : PaymentDecision.card(getBestCreatureAI(hand));
}
@Override
public PaymentDecision visit(CostChooseColor cost) {
int c = cost.getAbilityAmount(ability);
@@ -555,7 +563,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int thisRemove = Math.min(prefCard.getCounters(cType), stillToRemove);
if (thisRemove > 0) {
removed += thisRemove;
table.put(null, prefCard, CounterType.get(cType), thisRemove);
table.put(null, prefCard, cType, thisRemove);
}
}
}
@@ -565,7 +573,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
@Override
public PaymentDecision visit(CostRemoveAnyCounter cost) {
final int c = cost.getAbilityAmount(ability);
final Card originalHost = ObjectUtils.defaultIfNull(ability.getOriginalHost(), source);
final Card originalHost = ObjectUtils.getIfNull(ability.getOriginalHost(), source);
if (c <= 0) {
return null;
@@ -708,7 +716,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
int over = Math.min(crd.getCounters(CounterEnumType.QUEST) - e, c - toRemove);
if (over > 0) {
toRemove += over;
table.put(null, crd, CounterType.get(CounterEnumType.QUEST), over);
table.put(null, crd, CounterEnumType.QUEST, over);
}
}
}
@@ -759,6 +767,12 @@ public class AiCostDecision extends CostDecisionMakerBase {
public PaymentDecision visit(CostRemoveCounter cost) {
final String amount = cost.getAmount();
final String type = cost.getType();
final GameEntityCounterTable counterTable = new GameEntityCounterTable();
// TODO Help AI filter card with most useless counters and put those counters in countertable for things like
// Moxite Refinery, similar to CostRemoveAnyCounter
// Probably a lot of that decision making can be re-used or pulled out for both PaymentDecisions to use
if (cost.counter == null) return null;
int c;
@@ -787,7 +801,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
}
for (Card card : typeList) {
if (card.getCounters(cost.counter) >= c) {
return PaymentDecision.card(card, c);
counterTable.put(null, card, cost.counter, c);
return PaymentDecision.counters(counterTable);
}
}
return null;
@@ -798,7 +813,8 @@ public class AiCostDecision extends CostDecisionMakerBase {
return null;
}
return PaymentDecision.card(source, c);
counterTable.put(null, source, cost.counter, c);
return PaymentDecision.counters(counterTable);
}
@Override

View File

@@ -1,21 +1,52 @@
package forge.ai;
public enum AiPlayDecision {
// Play decision reasons
WillPlay,
MandatoryPlay,
PlayToEmptyHand,
ImpactCombat,
ResponseToStackResolve,
AddBoardPresence,
Removal,
Tempo,
CardAdvantage,
// Play later decisions
WaitForCombat,
WaitForMain2,
WaitForEndOfTurn,
StackNotEmpty,
AnotherTime,
// Don't play decision reasons
CantPlaySa,
CantPlayAi,
CantAfford,
CantAffordX,
WaitForMain2,
AnotherTime,
DoesntImpactCombat,
DoesntImpactGame,
MissingLogic,
MissingNeededCards,
TimingRestrictions,
MissingPhaseRestrictions,
ConditionsNotMet,
NeedsToPlayCriteriaNotMet,
StopRunawayActivations,
TargetingFailed,
CostNotAcceptable,
LifeInDanger,
WouldDestroyLegend,
WouldDestroyOtherPlaneswalker,
WouldBecomeZeroToughnessCreature,
WouldDestroyWorldEnchantment,
BadEtbEffects,
CurseEffects
CurseEffects;
public boolean willingToPlay() {
return switch (this) {
case WillPlay, MandatoryPlay, PlayToEmptyHand, AddBoardPresence, ImpactCombat, ResponseToStackResolve, Removal, Tempo, CardAdvantage -> true;
default -> false;
};
}
}

View File

@@ -48,6 +48,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility;
@@ -766,7 +767,7 @@ public class ComputerUtil {
public static CardCollection chooseUntapType(final Player ai, final String type, final Card activate, final boolean untap, final int amount, SpellAbility sa) {
CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), activate.getController(), activate, sa);
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterType.get(CounterEnumType.STUN)));
typeList = CardLists.filter(typeList, CardPredicates.TAPPED, c -> c.getCounters(CounterEnumType.STUN) == 0 || c.canRemoveCounters(CounterEnumType.STUN));
if (untap) {
typeList.remove(activate);
@@ -863,7 +864,7 @@ public class ComputerUtil {
// Run non-mandatory trigger.
// These checks only work if the Executing SpellAbility is an Ability_Sub.
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTriggerAI(ai, exSA, false)) {
if ((exSA instanceof AbilitySub) && !SpellApiToAi.Converter.get(exSA).doTrigger(ai, exSA, false)) {
// AI would not run this trigger if given the chance
return sacrificed;
}
@@ -1073,6 +1074,80 @@ public class ComputerUtil {
return prevented;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castPermanentInMain1(final Player ai, final SpellAbility sa) {
final Card card = sa.getHostCard();
final CardState cardState = card.isFaceDown() ? card.getState(CardStateName.Original) : card.getCurrentState();
@@ -1244,80 +1319,6 @@ public class ComputerUtil {
return false;
}
/**
* Is it OK to cast this for less than the Max Targets?
* @param source the source Card
* @return true if it's OK to cast this Card for less than the max targets
*/
public static boolean shouldCastLessThanMax(final Player ai, final Card source) {
if (source.getXManaCostPaid() > 0) {
// If TargetMax is MaxTgts (i.e., an "X" cost), this is fine because AI is limited by payment resources available.
return true;
}
if (aiLifeInDanger(ai, false, 0)) {
// Otherwise, if life is possibly in danger, then this is fine.
return true;
}
// do not play now.
return false;
}
/**
* Is this discard probably worse than a random draw?
* @param discard Card to discard
* @return boolean
*/
public static boolean isWorseThanDraw(final Player ai, Card discard) {
if (discard.hasSVar("DiscardMe")) {
return true;
}
final Game game = ai.getGame();
final CardCollection landsInPlay = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.LANDS_PRODUCING_MANA);
final CardCollection landsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.LANDS);
final CardCollection nonLandsInHand = CardLists.getNotType(ai.getCardsIn(ZoneType.Hand), "Land");
final int highestCMC = Math.max(6, Aggregates.max(nonLandsInHand, Card::getCMC));
final int discardCMC = discard.getCMC();
if (discard.isLand()) {
if (landsInPlay.size() >= highestCMC
|| (landsInPlay.size() + landsInHand.size() > 6 && landsInHand.size() > 1)
|| (landsInPlay.size() > 3 && nonLandsInHand.size() == 0)) {
// Don't need more land.
return true;
}
} else { //non-land
if (discardCMC > landsInPlay.size() + landsInHand.size() + 2) {
// not castable for some time.
return true;
} else if (!game.getPhaseHandler().isPlayerTurn(ai)
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.MAIN2)
&& discardCMC > landsInPlay.size() + landsInHand.size()
&& discardCMC > landsInPlay.size() + 1
&& nonLandsInHand.size() > 1) {
// not castable for at least one other turn.
return true;
} else if (landsInPlay.size() > 5 && discard.getCMC() <= 1
&& !discard.hasProperty("hasXCost", ai, null, null)) {
// Probably don't need small stuff now.
return true;
}
}
return false;
}
// returns true if it's better to wait until blockers are declared
public static boolean waitForBlocking(final SpellAbility sa) {
final Game game = sa.getActivatingPlayer().getGame();
final PhaseHandler ph = game.getPhaseHandler();
return sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases");
}
public static boolean castSpellInMain1(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final SpellAbility sub = sa.getSubAbility();
@@ -1326,7 +1327,6 @@ public class ComputerUtil {
return true;
}
// Cipher spells
if (sub != null) {
final ApiType api = sub.getApi();
if (ApiType.Encode == api && !ai.getCreaturesInPlay().isEmpty()) {
@@ -1384,13 +1384,14 @@ public class ComputerUtil {
// returns true if the AI should stop using the ability
public static boolean preventRunAwayActivations(final SpellAbility sa) {
int activations = sa.getActivationsThisTurn();
if (!sa.isIntrinsic()) {
return MyRandom.getRandom().nextFloat() >= .95; // Abilities created by static abilities have no memory
if (!sa.isActivatedAbility()) {
return false;
}
if (activations < 10) { //10 activations per turn should still be acceptable
int activations = sa.getActivationsThisTurn();
//10 activations should still be acceptable
if (activations < 10) {
return false;
}
@@ -1458,15 +1459,14 @@ public class ComputerUtil {
// check for Continuous abilities that grant Haste
for (final Card c : all) {
for (StaticAbility stAb : c.getStaticAbilities()) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("AddKeyword").contains("Haste")) {
if (c.isEquipment() && c.getEquipping() == null) {
return true;
}
final String affected = params.get("Affected");
final String affected = stAb.getParam("Affected");
if (affected.contains("Creature.YouCtrl")
|| affected.contains("Other+YouCtrl")) {
return true;
@@ -1519,11 +1519,10 @@ public class ComputerUtil {
for (final Card c : opp) {
for (StaticAbility stAb : c.getStaticAbilities()) {
Map<String, String> params = stAb.getMapParams();
if ("Continuous".equals(params.get("Mode")) && params.containsKey("AddKeyword")
&& params.get("AddKeyword").contains("Haste")) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddKeyword")
&& stAb.getParam("AddKeyword").contains("Haste")) {
final ArrayList<String> affected = Lists.newArrayList(params.get("Affected").split(","));
final ArrayList<String> affected = Lists.newArrayList(stAb.getParam("Affected").split(","));
if (affected.contains("Creature")) {
return true;
}
@@ -1622,7 +1621,6 @@ public class ComputerUtil {
damage = dmg;
}
// Triggered abilities
if (c.isCreature() && c.isInPlay() && CombatUtil.canAttack(c)) {
for (final Trigger t : c.getTriggers()) {
if (TriggerType.Attacks.equals(t.getMode())) {
@@ -2429,7 +2427,7 @@ public class ComputerUtil {
// Are we picking a type to reduce costs for that type?
boolean reducingCost = false;
for (StaticAbility s : sa.getHostCard().getStaticAbilities()) {
if ("ReduceCost".equals(s.getParam("Mode")) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
if (s.checkMode(StaticAbilityMode.ReduceCost) && "Card.ChosenType".equals(s.getParam("ValidCard"))) {
reducingCost = true;
break;
}
@@ -2544,7 +2542,7 @@ public class ComputerUtil {
boolean opponent = controller.isOpponentOf(ai);
final CounterType p1p1Type = CounterType.get(CounterEnumType.P1P1);
final CounterType p1p1Type = CounterEnumType.P1P1;
if (!sa.hasParam("AILogic")) {
return Aggregates.random(options);
@@ -2553,7 +2551,7 @@ public class ComputerUtil {
String logic = sa.getParam("AILogic");
switch (logic) {
case "Torture":
return "Torture";
return options.get(1);
case "GraceOrCondemnation":
List<ZoneType> graceZones = new ArrayList<ZoneType>();
graceZones.add(ZoneType.Battlefield);
@@ -2561,12 +2559,12 @@ public class ComputerUtil {
CardCollection graceCreatures = CardLists.getType(game.getCardsIn(graceZones), "Creature");
int humanGrace = CardLists.filterControlledBy(graceCreatures, ai.getOpponents()).size();
int aiGrace = CardLists.filterControlledBy(graceCreatures, ai).size();
return aiGrace > humanGrace ? "Grace" : "Condemnation";
return options.get(aiGrace > humanGrace ? 0 : 1);
case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists.getNotType(game.getCardsIn(ZoneType.Battlefield), "Land");
CardCollection humanlist = CardLists.filterControlledBy(cardsInPlay, ai.getOpponents());
CardCollection computerlist = ai.getCreaturesInPlay();
return ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? "Carnage" : "Homage";
return options.get(ComputerUtilCard.evaluatePermanentList(computerlist) + 3 < ComputerUtilCard.evaluatePermanentList(humanlist) ? 0 : 1);
case "Judgment":
if (votes.isEmpty()) {
CardCollection list = new CardCollection();
@@ -2580,68 +2578,71 @@ public class ComputerUtil {
return Iterables.getFirst(votes.keySet(), null);
case "Protection":
if (votes.isEmpty()) {
List<String> restrictedToColors = Lists.newArrayList();
Map<String, SpellAbility> restrictedToColors = Maps.newHashMap();
for (Object o : options) {
if (o instanceof String) {
restrictedToColors.add((String) o);
if (o instanceof SpellAbility sp) { // TODO check for Color Word Changes
restrictedToColors.put(sp.getOriginalDescription(), sp);
}
}
CardCollection lists = CardLists.filterControlledBy(game.getCardsInGame(), ai.getOpponents());
return StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors));
return restrictedToColors.get(StringUtils.capitalize(ComputerUtilCard.getMostProminentColor(lists, restrictedToColors.keySet())));
}
return Iterables.getFirst(votes.keySet(), null);
case "FeatherOrQuill":
SpellAbility feather = (SpellAbility)options.get(0);
SpellAbility quill = (SpellAbility)options.get(1);
// try to mill opponent with Quill vote
if (opponent && !controller.cantLoseCheck(GameLossReason.Milled)) {
int numQuill = votes.get("Quill").size();
int numQuill = votes.get(quill).size();
if (numQuill + 1 >= controller.getCardsIn(ZoneType.Library).size()) {
return controller.isCardInPlay("Laboratory Maniac") ? "Feather" : "Quill";
return controller.isCardInPlay("Laboratory Maniac") ? feather : quill;
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
}
// if source is not on the battlefield anymore, choose +1/+1 ones
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
}
// if no hand cards, try to mill opponent
if (controller.getCardsIn(ZoneType.Hand).isEmpty()) {
return opponent ? "Quill" : "Feather";
return opponent ? quill : feather;
}
// AI has something to discard
if (ai.equals(controller)) {
CardCollectionView aiCardsInHand = ai.getCardsIn(ZoneType.Hand);
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= 1) {
return "Quill";
return quill;
}
}
// default card draw and discard are better than +1/+1 counter
return opponent ? "Feather" : "Quill";
return opponent ? feather : quill;
case "StrengthOrNumbers":
SpellAbility strength = (SpellAbility)options.get(0);
SpellAbility numbers = (SpellAbility)options.get(1);
// similar to fabricate choose +1/+1 or Token
final SpellAbility saToken = sa.findSubAbilityByType(ApiType.Token);
int numStrength = votes.get("Strength").size();
int numNumbers = votes.get("Numbers").size();
int numStrength = votes.get(strength).size();
int numNumbers = votes.get(numbers).size();
Card token = TokenAi.spawnToken(controller, saToken);
Card token = TokenAi.spawnToken(controller, numbers);
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Strength" : "Numbers";
return opponent ? strength : numbers;
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Strength" : "Numbers";
return opponent ? strength : numbers;
}
// token would not survive
if (token == null || !token.isCreature() || token.getNetToughness() < 1) {
return opponent ? "Numbers" : "Strength";
return opponent ? numbers : strength;
}
// TODO check for ETB to +1/+1 counters or over another trigger like lifegain
@@ -2662,35 +2663,40 @@ public class ComputerUtil {
int scoreStrength = ComputerUtilCard.evaluateCreature(sourceStrength) + tokenScore * numNumbers;
int scoreNumbers = ComputerUtilCard.evaluateCreature(sourceNumbers) + tokenScore * (numNumbers + 1);
return (scoreNumbers >= scoreStrength) != opponent ? "Numbers" : "Strength";
return (scoreNumbers >= scoreStrength) != opponent ? numbers : strength;
case "SproutOrHarvest":
SpellAbility sprout = (SpellAbility)options.get(0);
SpellAbility harvest = (SpellAbility)options.get(1);
// lifegain would hurt or has no effect
if (opponent) {
if (lifegainNegative(controller, source)) {
return "Harvest";
return harvest;
}
} else {
if (lifegainNegative(controller, source)) {
return "Sprout";
return sprout;
}
}
// is it can't receive counters, choose +1/+1 ones
if (!source.canReceiveCounters(p1p1Type)) {
return opponent ? "Sprout" : "Harvest";
return opponent ? sprout : harvest;
}
// if source is not on the battlefield anymore
if (!game.getCardState(source).isInPlay()) {
return opponent ? "Sprout" : "Harvest";
return opponent ? sprout : harvest;
}
// TODO add Lifegain to +1/+1 counters trigger
// for now +1/+1 counters are better
return opponent ? "Harvest" : "Sprout";
return opponent ? harvest : sprout;
case "DeathOrTaxes":
int numDeath = votes.get("Death").size();
int numTaxes = votes.get("Taxes").size();
SpellAbility death = (SpellAbility)options.get(0);
SpellAbility taxes = (SpellAbility)options.get(1);
int numDeath = votes.get(death).size();
int numTaxes = votes.get(taxes).size();
if (opponent) {
CardCollection aiCreatures = ai.getCreaturesInPlay();
@@ -2698,29 +2704,29 @@ public class ComputerUtil {
// would need to sacrifice more creatures than AI has
// sacrifice even more
if (aiCreatures.size() <= numDeath) {
return "Death";
return death;
}
// would need to discard more cards than it has
if (aiCardsInHand.size() <= numTaxes) {
return "Taxes";
return taxes;
}
// has cards with SacMe or Token
if (CardLists.count(aiCreatures, CardPredicates.hasSVar("SacMe").or(CardPredicates.TOKEN)) >= numDeath) {
return "Death";
return death;
}
// has cards with DiscardMe
if (CardLists.count(aiCardsInHand, CardPredicates.hasSVar("DiscardMe")) >= numTaxes) {
return "Taxes";
return taxes;
}
// discard is probably less worse than sacrifice
return "Taxes";
return taxes;
} else {
// ai is first voter or ally of controller
// both are not affected, but if opponents control creatures, sacrifice is worse
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? "Taxes" : "Death";
return controller.getOpponents().getCreaturesInPlay().isEmpty() ? taxes : death;
}
default:
return Iterables.getFirst(options, null);
@@ -2891,7 +2897,7 @@ public class ComputerUtil {
// Iceberg does use Ice as Storage
|| (type.is(CounterEnumType.ICE) && !"Iceberg".equals(c.getName()))
// some lands does use Depletion as Storage Counter
|| (type.is(CounterEnumType.DEPLETION) && c.hasKeyword("CARDNAME doesn't untap during your untap step."))
|| (type.is(CounterEnumType.DEPLETION) && c.getReplacementEffects().anyMatch(r -> r.getMode().equals(ReplacementType.Untap) && r.getLayer().equals(ReplacementLayer.CantHappen)))
// treat Time Counters on suspended Cards as Bad,
// and also on Chronozoa
|| (type.is(CounterEnumType.TIME) && (!c.isInPlay() || "Chronozoa".equals(c.getName())))

View File

@@ -345,6 +345,10 @@ public class ComputerUtilAbility {
if (source.hasSVar("AIPriorityModifier")) {
p += Integer.parseInt(source.getSVar("AIPriorityModifier"));
}
// try to use it before it's gone
if (source.isInPlay() && source.hasSVar("EndOfTurnLeavePlay")) {
p += 1;
}
if (ComputerUtilCard.isCardRemAIDeck(sa.getOriginalHost() != null ? sa.getOriginalHost() : source)) {
p -= 10;
}

View File

@@ -48,6 +48,7 @@ import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.zone.MagicStack;
import forge.game.zone.ZoneType;
@@ -691,6 +692,8 @@ public class ComputerUtilCard {
public static boolean canBeBlockedProfitably(final Player ai, Card attacker, boolean checkingOther) {
AiBlockController aiBlk = new AiBlockController(ai, checkingOther);
Combat combat = new Combat(ai);
// avoid removing original attacker
attacker.setCombatLKI(null);
combat.addAttacker(attacker, ai);
final List<Card> attackers = Lists.newArrayList(attacker);
aiBlk.assignBlockersGivenAttackers(combat, attackers);
@@ -916,14 +919,14 @@ public class ComputerUtilCard {
return MagicColor.Constant.WHITE; // no difference, there was no prominent color
}
public static String getMostProminentColor(final CardCollectionView list, final List<String> restrictedToColors) {
public static String getMostProminentColor(final CardCollectionView list, final Iterable<String> restrictedToColors) {
byte colors = CardFactoryUtil.getMostProminentColorsFromList(list, restrictedToColors);
for (byte c : MagicColor.WUBRG) {
if ((colors & c) != 0) {
return MagicColor.toLongString(c);
}
}
return restrictedToColors.get(0); // no difference, there was no prominent color
return Iterables.get(restrictedToColors, 0); // no difference, there was no prominent color
}
public static List<String> getColorByProminence(final List<Card> list) {
@@ -1211,8 +1214,7 @@ public class ComputerUtilCard {
// if this thing is both owned and controlled by an opponent and it has a continuous ability,
// assume it either benefits the player or disrupts the opponent
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
if (params.get("Mode").equals("Continuous") && stAb.isIntrinsic()) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.isIntrinsic()) {
priority = true;
break;
}
@@ -1243,17 +1245,16 @@ public class ComputerUtilCard {
}
} else {
for (final StaticAbility stAb : c.getStaticAbilities()) {
final Map<String, String> params = stAb.getMapParams();
//continuous buffs
if (params.get("Mode").equals("Continuous") && "Creature.YouCtrl".equals(params.get("Affected"))) {
if (stAb.checkMode(StaticAbilityMode.Continuous) && "Creature.YouCtrl".equals(stAb.getParam("Affected"))) {
int bonusPT = 0;
if (params.containsKey("AddPower")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
if (stAb.hasParam("AddPower")) {
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
}
if (params.containsKey("AddToughness")) {
bonusPT += AbilityUtils.calculateAmount(c, params.get("AddPower"), stAb);
if (stAb.hasParam("AddToughness")) {
bonusPT += AbilityUtils.calculateAmount(c, stAb.getParam("AddPower"), stAb);
}
String kws = params.get("AddKeyword");
String kws = stAb.getParam("AddKeyword");
if (kws != null) {
bonusPT += 4 * (1 + StringUtils.countMatches(kws, "&")); //treat each added keyword as a +2/+2 for now
}
@@ -1784,7 +1785,7 @@ public class ComputerUtilCard {
// remove old boost that might be copied
for (final StaticAbility stAb : c.getStaticAbilities()) {
vCard.removePTBoost(c.getLayerTimestamp(), stAb.getId());
if (!stAb.checkMode("Continuous")) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
if (!stAb.hasParam("Affected")) {
@@ -1818,18 +1819,18 @@ public class ComputerUtilCard {
* @param sa Pump* or CounterPut*
* @return
*/
public static boolean canPumpAgainstRemoval(Player ai, SpellAbility sa) {
public static AiAbilityDecision canPumpAgainstRemoval(Player ai, SpellAbility sa) {
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa, true);
if (!sa.usesTargeting()) {
final List<Card> cards = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
for (final Card card : cards) {
if (objects.contains(card)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
}
}
// For pumps without targeting restrictions, just return immediately until this is fleshed out.
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
CardCollection threatenedTargets = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
@@ -1848,11 +1849,11 @@ public class ComputerUtilCard {
}
if (!sa.isTargetNumberValid()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
public static boolean isUselessCreature(Player ai, Card c) {
@@ -1862,7 +1863,7 @@ public class ComputerUtilCard {
if (!c.isCreature()) {
return false;
}
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.hasKeyword("CARDNAME doesn't untap during your untap step.") && c.isTapped()) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
if (c.hasKeyword("CARDNAME can't attack or block.") || (c.isTapped() && !c.canUntap(ai, true)) || (c.getOwner() == ai && ai.getOpponents().contains(c.getController()))) {
return true;
}
return false;

View File

@@ -31,7 +31,7 @@ import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment;
import forge.game.keyword.Keyword;
import forge.game.phase.Untap;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer;
@@ -39,6 +39,7 @@ import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityAssignCombatDamageAsUnblocked;
import forge.game.staticability.StaticAbilityMode;
import forge.game.staticability.StaticAbilityMustAttack;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
@@ -101,7 +102,7 @@ public class ComputerUtilCombat {
return false;
}
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), "BeginCombat")) {
if (attacker.getGame().getReplacementHandler().wouldPhaseBeSkipped(attacker.getController(), PhaseType.COMBAT_BEGIN)) {
return false;
}
@@ -118,7 +119,7 @@ public class ComputerUtilCombat {
// || (attacker.hasKeyword(Keyword.FADING) && attacker.getCounters(CounterEnumType.FADE) == 0)
// || attacker.hasSVar("EndOfTurnLeavePlay"));
// The creature won't untap next turn
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && Untap.canUntap(attacker));
return !attacker.isTapped() || (attacker.getCounters(CounterEnumType.STUN) == 0 && attacker.canUntap(attacker.getController(), true));
}
/**
@@ -214,7 +215,7 @@ public class ComputerUtilCombat {
int damage = attacker.getNetCombatDamage();
int poison = 0;
damage += predictPowerBonusOfAttacker(attacker, null, null, false);
if (attacker.hasKeyword(Keyword.INFECT)) {
if (attacker.isInfectDamage(attacked)) {
int pd = predictDamageTo(attacked, damage, attacker, true);
// opponent can always order it so that he gets 0
if (pd == 1 && attacker.getController().getOpponents().getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Vorinclex, Monstrous Raider"))) {
@@ -357,7 +358,7 @@ public class ComputerUtilCombat {
} else if (attacker.hasKeyword(Keyword.TRAMPLE)) {
int trampleDamage = getAttack(attacker) - totalShieldDamage(attacker, blockers);
if (trampleDamage > 0) {
if (attacker.hasKeyword(Keyword.INFECT)) {
if (attacker.isInfectDamage(ai)) {
poison += trampleDamage;
}
poison += predictExtraPoisonWithDamage(attacker, ai, trampleDamage);
@@ -900,7 +901,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode("Continuous")) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("blocking")) {
@@ -1196,7 +1197,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!stAb.checkMode("Continuous")) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
if (!stAb.hasParam("Affected") || !stAb.getParam("Affected").contains("attacking")) {
@@ -1387,7 +1388,7 @@ public class ComputerUtilCombat {
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
for (final Card card : cardList) {
for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!"Continuous".equals(stAb.getParam("Mode"))) {
if (!stAb.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
if (!stAb.hasParam("Affected")) {
@@ -1734,6 +1735,7 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about Deathtouch
if (blocker.hasDoubleStrike()) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
return true;
@@ -1963,6 +1965,7 @@ public class ComputerUtilCombat {
final int attackerLife = getDamageToKill(attacker, false)
+ predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
// AI should be less worried about deathtouch
if (attacker.hasDoubleStrike()) {
if (attackerDamage >= defenderLife) {
return true;

View File

@@ -5,6 +5,7 @@ import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import forge.game.GameObject;
import org.apache.commons.lang3.ObjectUtils;
import org.apache.commons.lang3.StringUtils;
@@ -139,11 +140,13 @@ public class ComputerUtilCost {
if (type.equals("CARDNAME")) {
if (source.getAbilityText().contains("Bloodrush")) {
continue;
} else if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
}
if (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize()) {
// Better do something than just discard stuff
return true;
}
return false;
}
typeList = CardLists.getValidCards(hand, type, source.getController(), source, sa);
if (typeList.size() > ai.getMaxHandSize()) {
@@ -248,11 +251,7 @@ public class ComputerUtilCost {
// Does the AI want to use Sacrifice All?
return false;
} else {
Integer c = part.convertAmount();
if (c == null) {
c = part.getAbilityAmount(sourceAbility);
}
int c = part.getAbilityAmount(sourceAbility);
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
CardCollectionView choices = aic.chooseSacrificeType(part.getType(), sourceAbility, effect, c, exclude);
if (choices != null) {
@@ -522,13 +521,12 @@ public class ComputerUtilCost {
sa.setActivatingPlayer(player); // complaints on NPE had came before this line was added.
}
boolean cannotBeCountered = false;
// Check for stuff like Nether Void
int extraManaNeeded = 0;
if (!effect) {
boolean cannotBeCountered = !sa.isCounterableBy(null);
if (sa instanceof Spell) {
cannotBeCountered = !sa.isCounterableBy(null);
for (Card c : player.getGame().getCardsIn(ZoneType.Battlefield)) {
final String snem = c.getSVar("AI_SpellsNeedExtraMana");
if (!StringUtils.isBlank(snem)) {
@@ -578,12 +576,24 @@ public class ComputerUtilCost {
}
}
// Ward - will be accounted for when rechecking a targeted ability
if (!sa.isTrigger() && (!sa.isSpell() || !cannotBeCountered)) {
// Account for possible Ward after the spell is fully targeted
// TODO: ideally, this should be done while targeting, so that a different target can be preferred if the best
// one is warded and can't be paid for. (currently it will be stuck with the target until it could pay)
if (!sa.isTrigger() && !cannotBeCountered) {
Set<GameObject> distinctObjects = Sets.newHashSet();
for (TargetChoices tc : sa.getAllTargetChoices()) {
for (Card tgt : tc.getTargetCards()) {
if (!distinctObjects.add(tgt)) {
continue;
}
// TODO some older cards don't use the keyword, so check for trigger instead
if (tgt.hasKeyword(Keyword.WARD) && tgt.isInPlay() && tgt.getController().isOpponentOf(sa.getHostCard().getController())) {
Cost wardCost = ComputerUtilCard.getTotalWardCost(tgt);
// don't use API converter since it might have special part logic not meant for Ward cost
SpellAbilityAi topAI = new SpellAbilityAi() {};
if (!topAI.willPayCosts(player, sa, wardCost, sa.getHostCard())) {
return false;
}
if (wardCost.hasManaCost()) {
extraManaNeeded += wardCost.getTotalMana().getCMC();
}
@@ -607,8 +617,9 @@ public class ComputerUtilCost {
}
}
// TODO both of these call CostAdjustment.adjust, try to reuse instead
return ComputerUtilMana.canPayManaCost(cost, sa, player, extraManaNeeded, effect)
&& CostPayment.canPayAdditionalCosts(cost, sa, effect);
&& CostPayment.canPayAdditionalCosts(cost, sa, effect, player);
}
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {

View File

@@ -158,7 +158,7 @@ public class ComputerUtilMana {
}
// Mana abilities on the same card
String shardMana = shard.toString().replaceAll("\\{", "").replaceAll("\\}", "");
String shardMana = shard.toShortString();
boolean payWithAb1 = ability1.getManaPart().mana(ability1).contains(shardMana);
boolean payWithAb2 = ability2.getManaPart().mana(ability2).contains(shardMana);
@@ -226,7 +226,7 @@ public class ComputerUtilMana {
}
public static SpellAbility chooseManaAbility(ManaCostBeingPaid cost, SpellAbility sa, Player ai, ManaCostShard toPay,
Collection<SpellAbility> saList, boolean checkCosts) {
Collection<SpellAbility> maList, boolean checkCosts) {
Card saHost = sa.getHostCard();
// CastTotalManaSpent (AIPreference:ManaFrom$Type or AIManaPref$ Type)
@@ -240,12 +240,12 @@ public class ComputerUtilMana {
manaSourceType = sa.getParam("AIManaPref");
}
if (manaSourceType != "") {
List<SpellAbility> filteredList = Lists.newArrayList(saList);
List<SpellAbility> filteredList = Lists.newArrayList(maList);
switch (manaSourceType) {
case "Snow":
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().isSnow()
&& ab2.getHostCard() != null && !ab2.getHostCard().isSnow() ? -1 : 1);
saList = filteredList;
maList = filteredList;
break;
case "Treasure":
// Try to spend only one Treasure if possible
@@ -253,22 +253,22 @@ public class ComputerUtilMana {
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
SpellAbility first = filteredList.get(0);
if (first.getHostCard() != null && first.getHostCard().getType().hasSubtype("Treasure")) {
saList.remove(first);
maList.remove(first);
List<SpellAbility> updatedList = Lists.newArrayList();
updatedList.add(first);
updatedList.addAll(saList);
saList = updatedList;
updatedList.addAll(maList);
maList = updatedList;
}
break;
case "TreasureMax":
// Ok to spend as many Treasures as possible
filteredList.sort((ab1, ab2) -> ab1.getHostCard() != null && ab1.getHostCard().getType().hasSubtype("Treasure")
&& ab2.getHostCard() != null && !ab2.getHostCard().getType().hasSubtype("Treasure") ? -1 : 1);
saList = filteredList;
maList = filteredList;
break;
case "NotSameCard":
String hostName = sa.getHostCard().getName();
saList = filteredList.stream()
maList = filteredList.stream()
.filter(saPay -> !saPay.getHostCard().getName().equals(hostName))
.collect(Collectors.toList());
break;
@@ -277,7 +277,7 @@ public class ComputerUtilMana {
}
}
for (final SpellAbility ma : saList) {
for (final SpellAbility ma : maList) {
// this rarely seems like a good idea
if (ma.getHostCard() == saHost) {
continue;
@@ -336,7 +336,7 @@ public class ComputerUtilMana {
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
continue;
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
for (SpellAbility ab : saList) {
for (SpellAbility ab : maList) {
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
if (!ab.getHostCard().isTapped()) {
paymentChoice = ab;
@@ -590,12 +590,12 @@ public class ComputerUtilMana {
while (!cost.isPaid()) {
toPay = getNextShardToPay(cost, sourcesForShards);
Collection<SpellAbility> saList = sourcesForShards.get(toPay);
if (saList == null) {
Collection<SpellAbility> maList = sourcesForShards.get(toPay);
if (maList == null) {
break;
}
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, saList, true);
SpellAbility saPayment = chooseManaAbility(cost, sa, ai, toPay, maList, true);
if (saPayment == null) {
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)) {
@@ -642,7 +642,8 @@ public class ComputerUtilMana {
List<SpellAbility> paymentList = Lists.newArrayList();
final ManaPool manapool = ai.getManaPool();
// Apply the color/type conversion matrix if necessary
// Apply color/type conversion matrix if necessary (already done via autopay)
if (ai.getControllingPlayer() == null) {
manapool.restoreColorReplacements();
CardPlayOption mayPlay = sa.getMayPlayOption();
if (!effect) {
@@ -656,12 +657,16 @@ public class ComputerUtilMana {
AbilityUtils.applyManaColorConversion(manapool, sa.getParam("ManaConversion"));
}
StaticAbilityManaConvert.manaConvert(manapool, ai, sa.getHostCard(), effect && !sa.isCastFromPlayEffect() ? null : sa);
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
CostPayment.handleOfferings(sa, test, cost.isPaid());
return true; // paid all from floating mana
}
// not worth checking if it makes sense to not spend floating first
if (manapool.payManaCostFromPool(cost, sa, test, manaSpentToPay)) {
CostPayment.handleOfferings(sa, test, cost.isPaid());
// paid all from floating mana
return true;
}
int phyLifeToPay = 2;
boolean purePhyrexian = cost.containsOnlyPhyrexianMana();
boolean hasConverge = sa.getHostCard().hasConverge();
ListMultimap<ManaCostShard, SpellAbility> sourcesForShards = getSourcesForShards(cost, sa, ai, test, checkPlayable, hasConverge);
@@ -689,13 +694,12 @@ public class ComputerUtilMana {
}
if (sourcesForShards == null && !purePhyrexian) {
break; // no mana abilities to use for paying
// no mana abilities to use for paying
break;
}
toPay = getNextShardToPay(cost, sourcesForShards);
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
Collection<SpellAbility> saList = null;
if (hasConverge &&
(toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)) {
@@ -748,9 +752,14 @@ public class ComputerUtilMana {
}
if (saPayment == null) {
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(2, false, sa)
|| (ai.getLife() <= 2 && !ai.cantLoseForZeroOrLessLife())) {
break; // cannot pay
boolean lifeInsteadOfBlack = toPay.isBlack() && ai.hasKeyword("PayLifeInsteadOf:B");
if ((!toPay.isPhyrexian() && !lifeInsteadOfBlack) || !ai.canPayLife(phyLifeToPay, false, sa)
|| (ai.getLife() <= phyLifeToPay && !ai.cantLoseForZeroOrLessLife())) {
// cannot pay
break;
}
if (test) {
phyLifeToPay += 2;
}
if (sa.hasParam("AIPhyrexianPayment")) {
@@ -954,7 +963,6 @@ public class ComputerUtilMana {
if (checkCosts) {
// Check if AI can still play this mana ability
ma.setActivatingPlayer(ai);
// if the AI can't pay the additional costs skip the mana ability
if (!CostPayment.canPayAdditionalCosts(ma.getPayCosts(), ma, false)) {
return false;
} else if (ma.getRestrictions() != null && ma.getRestrictions().isInstantSpeed()) {
@@ -972,9 +980,10 @@ public class ComputerUtilMana {
continue;
}
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s)))
if ("Any".equals(s) || ai.getManaPool().canPayForShardWithColor(toPay, ManaAtom.fromName(s))){
return true;
}
}
return false;
}
@@ -1499,7 +1508,7 @@ public class ComputerUtilMana {
AbilitySub sub = m.getSubAbility();
// We really shouldn't be hardcoding names here. ChkDrawback should just return true for them
if (sub != null && !card.getName().equals("Pristine Talisman") && !card.getName().equals("Zhur-Taa Druid")) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
needsLimitedResources = true; // TODO: check for good drawbacks (gainLife)
@@ -1579,7 +1588,7 @@ public class ComputerUtilMana {
// don't use abilities with dangerous drawbacks
AbilitySub sub = m.getSubAbility();
if (sub != null) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub)) {
if (!SpellApiToAi.Converter.get(sub).chkDrawbackWithSubs(ai, sub).willingToPlay()) {
continue;
}
}

View File

@@ -160,12 +160,6 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(20, "protection");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
// paired creatures are more valuable because they grant a bonus to the other creature
if (c.isPaired()) {
value += addValue(14, "paired");
@@ -213,11 +207,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(1, "untapped");
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
if (c.hasKeyword("CARDNAME doesn't untap during your untap step.")) {
if (!c.canUntap(c.getController(), true)) {
if (c.isTapped()) {
value = addValue(50 + (c.getCMC() * 5), "tapped-useless"); // reset everything - useless
} else {
@@ -226,6 +216,17 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} else {
value -= subValue(10 * c.getCounters(CounterEnumType.STUN), "stunned");
}
for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) {
value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
}
}
if (!c.getManaAbilities().isEmpty()) {
value += addValue(10, "manadork");
}
// use scaling because the creature is only available halfway
if (c.hasKeyword(Keyword.PHASING)) {
value -= subValue(Math.max(20, value / 2), "phasing");

View File

@@ -13,6 +13,7 @@ import forge.card.mana.ManaAtom;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.ability.AbilityFactory;
import forge.game.ability.ApiType;
import forge.game.ability.effects.DetachedCardEffect;
import forge.game.card.*;
import forge.game.card.token.TokenInfo;
@@ -263,12 +264,14 @@ public abstract class GameState {
}
if (c.hasMergedCard()) {
String suffix = c.getTopMergedCard().hasPaperFoil() ? "+" : "";
// we have to go by the current top card name here
newText.append(c.getTopMergedCard().getPaperCard().getName()).append("|Set:")
newText.append(c.getTopMergedCard().getPaperCard().getName()).append(suffix).append("|Set:")
.append(c.getTopMergedCard().getPaperCard().getEdition()).append("|Art:")
.append(c.getTopMergedCard().getPaperCard().getArtIndex());
} else {
newText.append(c.getPaperCard().getName()).append("|Set:").append(c.getPaperCard().getEdition())
String suffix = c.hasPaperFoil() ? "+" : "";
newText.append(c.getPaperCard().getName()).append(suffix).append("|Set:").append(c.getPaperCard().getEdition())
.append("|Art:").append(c.getPaperCard().getArtIndex());
}
}
@@ -318,18 +321,21 @@ public abstract class GameState {
newText.append(":Cloaked");
}
}
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
if (c.getMeldedWith() != null) {
String suffix = c.getMeldedWith().hasPaperFoil() ? "+" : "";
newText.append(":");
newText.append(c.getMeldedWith().getName());
newText.append(c.getMeldedWith().getName()).append(suffix);
}
} else if (c.getCurrentStateName().equals(CardStateName.Modal)) {
} else if (c.getCurrentStateName().equals(CardStateName.Backside)) {
if (c.isModal()) {
newText.append("|Modal");
} else {
newText.append("|Transformed");
}
}
if (c.getPlayerAttachedTo() != null) {
@@ -1262,7 +1268,7 @@ public abstract class GameState {
} else if (cardinfo[0].startsWith("T:")) {
String tokenStr = cardinfo[0].substring(2);
PaperToken token = StaticData.instance().getAllTokens().getToken(tokenStr,
setCode != null ? setCode : CardEdition.UNKNOWN.getName());
setCode != null ? setCode : CardEdition.UNKNOWN_CODE);
if (token == null) {
System.err.println("ERROR: Tried to create a non-existent token named " + cardinfo[0] + " when loading game state!");
continue;
@@ -1305,13 +1311,13 @@ public abstract class GameState {
} else if (info.startsWith("FaceDown")) {
c.turnFaceDown(true);
if (info.endsWith("Manifested")) {
c.setManifested(true);
c.setManifested(new SpellAbility.EmptySa(ApiType.Manifest, c));
}
if (info.endsWith("Cloaked")) {
c.setCloaked(true);
c.setCloaked(new SpellAbility.EmptySa(ApiType.Cloak, c));
}
} else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true);
} else if (info.startsWith("Transformed") || info.startsWith("Modal")) {
c.setState(CardStateName.Backside, true);
c.setBackSide(true);
} else if (info.startsWith("Flipped")) {
c.setState(CardStateName.Flipped, true);
@@ -1329,9 +1335,6 @@ public abstract class GameState {
}
c.setState(CardStateName.Meld, true);
c.setBackSide(true);
} else if (info.startsWith("Modal")) {
c.setState(CardStateName.Modal, true);
c.setBackSide(true);
}
else if (info.startsWith("OnAdventure")) {
String abAdventure = "DB$ Effect | RememberObjects$ Self | StaticAbilities$ Play | ForgetOnMoved$ Exile | Duration$ Permanent | ConditionDefined$ Self | ConditionPresent$ Card.!copiedSpell";
@@ -1408,7 +1411,7 @@ public abstract class GameState {
} else if (info.equals("Foretold")) {
c.setForetold(true);
c.turnFaceDown(true);
c.addMayLookTemp(c.getOwner());
c.addMayLookFaceDownExile(c.getOwner());
} else if (info.equals("ForetoldThisTurn")) {
c.setTurnInZone(turn);
} else if (info.equals("IsToken")) {

View File

@@ -15,6 +15,7 @@ import forge.game.*;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect;
import forge.game.ability.effects.RollDiceEffect;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
@@ -745,6 +746,30 @@ public class PlayerControllerAi extends PlayerController {
return Aggregates.random(rolls);
}
@Override
public List<Integer> chooseDiceToReroll(List<Integer> rolls) {
//TODO create AI logic for this
return new ArrayList<>();
}
@Override
public Integer chooseRollToModify(List<Integer> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public RollDiceEffect.DieRollResult chooseRollToSwap(List<RollDiceEffect.DieRollResult> rolls) {
//TODO create AI logic for this
return Aggregates.random(rolls);
}
@Override
public String chooseRollSwapValue(List<String> swapChoices, Integer currentResult, int power, int toughness) {
//TODO create AI logic for this
return Aggregates.random(swapChoices);
}
@Override
public boolean mulliganKeepHand(Player firstPlayer, int cardsToReturn) {
return !ComputerUtil.wantMulligan(player, cardsToReturn);
@@ -1207,6 +1232,11 @@ public class PlayerControllerAi extends PlayerController {
return false;
}
public boolean payCostDuringRoll(final Cost cost, final SpellAbility sa, final FCollectionView<Player> allPayers) {
// TODO logic for AI to pay rerolls and modification costs
return false;
}
@Override
public void orderAndPlaySimultaneousSa(List<SpellAbility> activePlayerSAs) {
for (final SpellAbility sa : getAi().orderPlaySa(activePlayerSAs)) {
@@ -1389,11 +1419,11 @@ public class PlayerControllerAi extends PlayerController {
oppLibrary = CardLists.getValidCards(oppLibrary, valid, source.getController(), source, sa);
}
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
if (source != null && source.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
// If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name)
for (Card consp : player.getCardsIn(ZoneType.Command)) {
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
if (consp.getState(CardStateName.Original).hasKeyword(Keyword.HIDDEN_AGENDA)) {
String chosenName = consp.getNamedCard();
if (!chosenName.isEmpty()) {
aiLibrary = CardLists.filter(aiLibrary, CardPredicates.nameNotEquals(chosenName));

View File

@@ -171,7 +171,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}
@@ -214,7 +214,7 @@ public class SpecialAiLogic {
}
// A logic for cards that say "Sacrifice a creature: put X +1/+1 counters on CARDNAME" (e.g. Falkenrath Aristocrat)
public static boolean doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision doAristocratWithCountersLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic"); // should not even get here unless there's an Aristocrats logic applied
final boolean isDeclareBlockers = ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS);
@@ -222,14 +222,14 @@ public class SpecialAiLogic {
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
if (numOtherCreats == 0) {
// Cut short if there's nothing to sac at all
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
// Check if the standard Aristocrats logic applies first (if in the right conditions for it)
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
if (isDeclareBlockers || isThreatened) {
if (doAristocratLogic(ai, sa)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -247,7 +247,7 @@ public class SpecialAiLogic {
if (countersSa == null) {
// Shouldn't get here if there is no PutCounter subability (wrong AI logic specified?)
System.err.println("Warning: AILogic AristocratCounters was specified on " + source + ", but there was no PutCounter SA in chain!");
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
final Game game = ai.getGame();
@@ -263,7 +263,7 @@ public class SpecialAiLogic {
relevantCreats.remove(source);
if (relevantCreats.isEmpty()) {
// No relevant creatures to sac
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
int numCtrs = AbilityUtils.calculateAmount(source, countersSa.getParam("CounterNum"), countersSa);
@@ -277,7 +277,7 @@ public class SpecialAiLogic {
final boolean isInfect = source.hasKeyword(Keyword.INFECT);
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterEnumType.POISON)) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}
@@ -287,16 +287,20 @@ public class SpecialAiLogic {
|| (combat.isAttacking(card) && combat.isBlocked(card) && ComputerUtilCombat.combatantWouldBeDestroyed(ai, card, combat))
);
if (!forcedSacTgts.isEmpty()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
final int numCreatsToSac = Math.max(0, (lethalDmg - source.getNetCombatDamage()) / numCtrs);
if (defTappedOut || numCreatsToSac < relevantCreats.size() / 2) {
return source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg;
if (source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + relevantCreats.size() * numCtrs >= lethalDmg) {
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
@@ -309,7 +313,7 @@ public class SpecialAiLogic {
);
if (sacTgts.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
final boolean sourceCantDie = ComputerUtilCombat.combatantCantBeDestroyed(ai, source);
@@ -317,7 +321,10 @@ public class SpecialAiLogic {
final int DefP = sourceCantDie ? 0 : Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
if (source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
@@ -329,7 +336,11 @@ public class SpecialAiLogic {
|| ComputerUtil.predictThreatenedObjects(ai, null, true).contains(card)
);
return !sacFodder.isEmpty();
if (sacFodder.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -360,10 +371,10 @@ public class SpecialAiLogic {
// FIXME: We're emulating the UnlessCost on the SA to run the proper checks.
// This is hacky, but it works. Perhaps a cleaner way exists?
sa.getMapParams().put("UnlessCost", falseSub.getParam("UnlessCost"));
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
sa.getMapParams().remove("UnlessCost");
} else {
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayAIWithSubs(ai, sa);
willPlay = SpellApiToAi.Converter.get(ApiType.Counter).canPlayWithSubs(ai, sa).willingToPlay();
}
return willPlay;
}

View File

@@ -78,16 +78,17 @@ public class SpecialCardAi {
// Arena and Magus of the Arena
public static class Arena {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
// TODO This is basically removal, so we may want to play this at other times
if (!game.getPhaseHandler().is(PhaseType.END_OF_TURN) || game.getPhaseHandler().getNextTurn() != ai) {
return false; // at opponent's EOT only, to conserve mana
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
CardCollection aiCreatures = ai.getCreaturesInPlay();
if (aiCreatures.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
for (Player opp : ai.getOpponents()) {
@@ -111,11 +112,11 @@ public class SpecialCardAi {
if (canKillAll) {
sa.getTargets().clear();
sa.getTargets().add(aiCreature);
return true;
return new AiAbilityDecision(100, AiPlayDecision.Removal);
}
}
}
return sa.isTargetNumberValid();
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -203,7 +204,7 @@ public class SpecialCardAi {
// Chain of Acid
public static class ChainOfAcid {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.LANDS);
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
@@ -213,13 +214,22 @@ public class SpecialCardAi {
// which it can only distinguish by their CMC, considering >CMC higher value).
// Currently ensures that the AI will still have lands provided that the human player goes to
// destroy all the AI's lands in order (to avoid manalock).
return !OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2;
if (!OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2) {
// If there are enough lands, target the worst non-creature permanent of the opponent
Card worstOppPerm = ComputerUtilCard.getWorstAI(OppPerms);
if (worstOppPerm != null) {
sa.resetTargets();
sa.getTargets().add(worstOppPerm);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
// Chain of Smog
public static class ChainOfSmog {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
if (ai.getCardsIn(ZoneType.Hand).isEmpty()) {
// to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point)
// TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a
@@ -235,10 +245,10 @@ public class SpecialCardAi {
sa.getParent().resetTargets();
sa.getParent().getTargets().add(targOpp);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
@@ -359,18 +369,28 @@ public class SpecialCardAi {
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
Card c = sa.getHostCard();
// Only check for sacrifice if it's the owner's turn, and it can attack.
// TODO: Maybe check if sacrificing a creature allows AI to kill the opponent with the rest on their turn?
if (!CombatUtil.canAttack(c) ||
!ai.getGame().getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return false;
}
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.UNTAPPED.and(
CardPredicates.hasKeyword(Keyword.FLYING).or(CardPredicates.hasKeyword(Keyword.REACH))));
boolean hasUsefulBlocker = false;
for (Card c : flyingCreatures) {
if (!ComputerUtilCard.isUselessCreature(ai, c)) {
for (Card fc : flyingCreatures) {
if (!ComputerUtilCard.isUselessCreature(ai, fc)) {
hasUsefulBlocker = true;
break;
}
}
return ai.getLife() <= sa.getHostCard().getNetPower() && !hasUsefulBlocker;
return ai.getLife() <= c.getNetPower() && !hasUsefulBlocker;
}
public static int getSacThreshold() {
@@ -380,7 +400,7 @@ public class SpecialCardAi {
// Donate
public static class Donate {
public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision considerTargetingOpponent(final Player ai, final SpellAbility sa) {
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
@@ -390,7 +410,7 @@ public class SpecialCardAi {
// All opponents have hexproof or something like that
if (Iterables.isEmpty(oppList)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// filter for player who does not have donate target already
@@ -408,31 +428,30 @@ public class SpecialCardAi {
if (opp != null) {
sa.resetTargets();
sa.getTargets().add(opp);
return true;
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// No targets found to donate, so do nothing.
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
public static boolean considerDonatingPermanent(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision considerDonatingPermanent(final Player ai, final SpellAbility sa) {
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) {
sa.resetTargets();
sa.getTargets().add(donateTarget);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// Should never get here because targetOpponent, called before targetPermanentToDonate, should already have made the AI bail
System.err.println("Warning: Donate AI failed at SpecialCardAi.Donate#targetPermanentToDonate despite successfully targeting an opponent first.");
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
// Electrostatic Pummeler
public static class ElectrostaticPummeler {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
Game game = ai.getGame();
Combat combat = game.getCombat();
@@ -445,13 +464,13 @@ public class SpecialCardAi {
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop);
if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg)
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
// Do not activate if damage will be prevented
if (source.staticDamagePrevention(predictedPT.getLeft(), 0, source, true) == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
}
// Activate Electrostatic Pummeler's pump only as a combat trick
@@ -460,14 +479,14 @@ public class SpecialCardAi {
// We'll try to deal lethal trample/unblocked damage, so remember the card for attack
// and wait until declare blockers step.
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean isBlocking = combat.isBlocking(source);
@@ -492,11 +511,11 @@ public class SpecialCardAi {
}
if (totalDamageToPW >= oppT + loyalty) {
// Already enough damage to take care of the planeswalker
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) {
// Can pump to kill the planeswalker, go for it
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}
@@ -517,31 +536,31 @@ public class SpecialCardAi {
// We can deal a lot of damage (either a lot of damage directly to the opponent,
// or kill the blocker(s) and damage the opponent at the same time, so go for it
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}
if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) {
// Can't survive first strike or double strike, don't pump
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) {
// Can't pump enough to kill the blockers and survive, don't pump
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) {
// Already enough to kill the blockers and survive, don't overpump
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (oppCantDie && !source.hasKeyword(Keyword.TRAMPLE) && !source.isWitherDamage()
&& predictedPT.getLeft() <= oppT) {
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
// If we got here, it should be a favorable combat pump, resulting in at least one
// opposing creature dying, and hopefully with the Pummeler surviving combat.
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) {
@@ -618,15 +637,15 @@ public class SpecialCardAi {
// Fell the Mighty
public static class FellTheMighty {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
CardCollection aiList = ai.getCreaturesInPlay();
if (aiList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
CardLists.sortByPowerAsc(aiList);
Card lowest = aiList.get(0);
if (!sa.canTarget(lowest)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
CardCollection oppList = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield),
@@ -636,9 +655,9 @@ public class SpecialCardAi {
if (ComputerUtilCard.evaluateCreatureList(oppList) > 200) {
sa.resetTargets();
sa.getTargets().add(lowest);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
@@ -673,25 +692,25 @@ public class SpecialCardAi {
// Gideon Blackblade
public static class GideonBlackblade {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
sa.resetTargets();
CardCollectionView otb = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isTargetableBy(sa));
if (!otb.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(otb));
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
// Goblin Polka Band
public static class GoblinPolkaBand {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
int maxPotentialTgts = ai.getOpponents().getCreaturesInPlay().filter(CardPredicates.UNTAPPED).size();
int maxPotentialPayment = ComputerUtilMana.determineLeftoverMana(sa, ai, "R", false);
int numTgts = Math.min(maxPotentialPayment, maxPotentialTgts);
if (numTgts == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// Set Announce
@@ -701,7 +720,7 @@ public class SpecialCardAi {
List<GameEntity> validTgts = sa.getTargetRestrictions().getAllCandidates(sa, true);
sa.resetTargets();
sa.getTargets().addAll(Aggregates.random(validTgts, numTgts));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -910,12 +929,12 @@ public class SpecialCardAi {
// Living Death (and other similar cards using AILogic LivingDeath or AILogic ReanimateAll)
public static class LivingDeath {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
// if there's another reanimator card currently suspended, don't cast a new one until the previous
// one resolves, otherwise the reanimation attempt will be ruined (e.g. Living End)
for (Card ex : ai.getCardsIn(ZoneType.Exile)) {
if (ex.hasSVar("IsReanimatorCard") && ex.getCounters(CounterEnumType.TIME) > 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -926,7 +945,7 @@ public class SpecialCardAi {
if (aiCreaturesInGY.isEmpty()) {
// nothing in graveyard, so cut short
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
for (Card c : ai.getCreaturesInPlay()) {
@@ -958,17 +977,30 @@ public class SpecialCardAi {
}
// if we get more value out of this than our opponent does (hopefully), go for it
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold);
if ((aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
// Maze's End
public static class MazesEnd {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
CardCollection availableGates = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.isType("Gate"));
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty();
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai && !availableGates.isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (availableGates.isEmpty()) {
// No gates available, so don't activate Maze's End
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
public static Card considerCardToGet(final Player ai, final SpellAbility sa)
@@ -1032,29 +1064,33 @@ public class SpecialCardAi {
return exiledWith == null || (tgt != null && ComputerUtilCard.evaluateCreature(tgt) > ComputerUtilCard.evaluateCreature(exiledWith));
}
public static boolean considerCopy(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision considerCopy(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final Card exiledWith = source.getImprintedCards().isEmpty() ? null : source.getImprintedCards().getFirst();
if (exiledWith == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
// We want to either be able to attack with the creature, or keep it until our opponent's end of turn as a
// potential blocker
return ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
if (ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, exiledWith)
|| (ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai) && ai.getGame().getCombat() != null
&& !ai.getGame().getCombat().getAttackers().isEmpty());
&& !ai.getGame().getCombat().getAttackers().isEmpty())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
// Momir Vig, Simic Visionary Avatar
public static class MomirVigAvatar {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
// In MoJhoSto, prefer Jhoira sorcery ability from time to time
@@ -1065,7 +1101,7 @@ public class SpecialCardAi {
int numLandsForJhoira = aic.getIntProperty(AiProps.MOJHOSTO_NUM_LANDS_TO_ACTIVATE_JHOIRA);
if (ai.getLandsInPlay().size() >= numLandsForJhoira && MyRandom.percentTrue(chanceToPrefJhoira)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
}
@@ -1074,7 +1110,7 @@ public class SpecialCardAi {
// Some basic strategy for Momir
if (tokenSize < 2) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
if (tokenSize > 11) {
@@ -1083,7 +1119,7 @@ public class SpecialCardAi {
sa.setXManaCostPaid(tokenSize);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -1122,13 +1158,13 @@ public class SpecialCardAi {
// Necropotence
public static class Necropotence {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
Game game = ai.getGame();
int computerHandSize = ai.getZone(ZoneType.Hand).size();
int maxHandSize = ai.getMaxHandSize();
if (ai.getCardsIn(ZoneType.Library).isEmpty()) {
return false; // nothing to draw from the library
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (ai.getCardsIn(ZoneType.Battlefield).anyMatch(CardPredicates.nameEquals("Yawgmoth's Bargain"))) {
@@ -1136,7 +1172,7 @@ public class SpecialCardAi {
// TODO: in presence of bad effects which deal damage when a card is drawn, probably better to prefer Necropotence instead?
// (not sure how to detect the presence of such effects yet)
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
PhaseHandler ph = game.getPhaseHandler();
@@ -1158,23 +1194,33 @@ public class SpecialCardAi {
// We're in a situation when we have nothing castable in hand, something needs to be done
if (!blackViseOTB) {
// exile-loot +1 card when at max hand size, hoping to get a workable spell or land
return computerHandSize + exiledWithNecro - 1 == maxHandSize;
if (computerHandSize + exiledWithNecro - 1 == maxHandSize) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// Loot to 7 in presence of Black Vise, hoping to find what to do
// NOTE: can still currently get theoretically locked with 7 uncastable spells. Loot to 8 instead?
return computerHandSize + exiledWithNecro <= maxHandSize;
if (computerHandSize + exiledWithNecro <= maxHandSize) {
// Loot to 7, hoping to find something playable
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// Loot to 8, hoping to find something playable
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else if (blackViseOTB && computerHandSize + exiledWithNecro - 1 >= 4) {
// try not to overdraw in presence of Black Vise
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (computerHandSize + exiledWithNecro - 1 >= maxHandSize) {
// Only draw until we reach max hand size
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (!ph.isPlayerTurn(ai) || !ph.is(PhaseType.MAIN2)) {
// Only activate in AI's own turn (sans the exception above)
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -1294,7 +1340,7 @@ public class SpecialCardAi {
}
}
public static boolean considerSecondTarget(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision considerSecondTarget(final Player ai, final SpellAbility sa) {
Card firstTgt = sa.getParent().getTargetCard();
CardCollection candidates = ai.getOpponents().getCardsIn(ZoneType.Battlefield).filter(
CardPredicates.sharesCardTypeWith(firstTgt).and(CardPredicates.isTargetableBy(sa)));
@@ -1302,23 +1348,25 @@ public class SpecialCardAi {
if (secondTgt != null) {
sa.resetTargets();
sa.getTargets().add(secondTgt);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
// Price of Progress
public static class PriceOfProgress {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
// Don't play in early game - opponent likely still has lands to play
if (ai.getGame().getPhaseHandler().getTurn() < 10) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
int aiLands = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
// TODO Better if we actually calculate the true damage
boolean willDieToPCasting = (ai.getLife() <= aiLands * 2);
if (!willDieToPCasting) {
boolean hasBridge = false;
for (Card c : ai.getCardsIn(ZoneType.Battlefield)) {
// Do we have a card in play that makes us want to empty out hand?
@@ -1330,61 +1378,75 @@ public class SpecialCardAi {
// Do if we need to lose cards to activate Ensnaring Bridge or Cursed Scroll
// even if suboptimal play, but don't waste the card too early even then!
if ((hasBridge) && (ai.getGame().getPhaseHandler().getTurn() >= 10)) {
return true;
if (hasBridge) {
return new AiAbilityDecision(100, AiPlayDecision.PlayToEmptyHand);
}
}
boolean willPlay = true;
for (Player opp : ai.getOpponents()) {
int oppLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield), CardPredicates.NONBASIC_LANDS).size();
// Don't if no enemy nonbasic lands
if (oppLands == 0) {
willPlay = false;
continue;
}
// Always if enemy would die and we don't!
// TODO : predict actual damage instead of assuming it'll be 2*lands
// Don't if we lose, unless we lose anyway to unblocked creatures next turn
if ((ai.getLife() <= aiLands * 2) &&
if (willDieToPCasting &&
(!(ComputerUtil.aiLifeInDanger(ai, true, 0)) && ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2))) {
return false;
willPlay = false;
}
// Do if we can win
if ((ai.getOpponentsSmallestLifeTotal()) <= oppLands * 2) {
return true;
if (opp.getLife() <= oppLands * 2) {
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
}
// Don't if we'd lose a larger percentage of our remaining life than enemy
if ((aiLands / ((double) ai.getLife())) >
(oppLands / ((double) ai.getOpponentsSmallestLifeTotal()))) {
return false;
}
// Don't if no enemy nonbasic lands
if (oppLands == 0) {
return false;
willPlay = false;
}
// Don't if loss is equal in percentage but we lose more points
if (((aiLands / ((double) ai.getLife())) == (oppLands / ((double) ai.getOpponentsSmallestLifeTotal())))
&& (aiLands > oppLands)) {
return false;
willPlay = false;
}
}
return true;
if (willPlay) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
// Sarkhan the Mad
public static class SarkhanTheMad {
public static boolean considerDig(final Player ai, final SpellAbility sa) {
return sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1;
public static AiAbilityDecision considerDig(final Player ai, final SpellAbility sa) {
if (sa.getHostCard().getCounters(CounterEnumType.LOYALTY) == 1) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision considerMakeDragon(final Player ai, final SpellAbility sa) {
// TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier?
CardCollection creatures = ai.getCreaturesInPlay();
boolean hasValidTgt = !CardLists.filter(creatures, t -> t.getNetPower() < 5 && t.getNetToughness() < 5).isEmpty();
if (hasValidTgt) {
Card worstCreature = ComputerUtilCard.getWorstCreatureAI(creatures);
sa.getTargets().add(worstCreature);
return true;
return new AiAbilityDecision(100, AiPlayDecision.AddBoardPresence);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) {
int minLife = weakestOpp.getLife();
@@ -1440,7 +1502,7 @@ public class SpecialCardAi {
// Sorin, Vengeful Bloodlord
public static class SorinVengefulBloodlord {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
int loyalty = sa.getHostCard().getCounters(CounterEnumType.LOYALTY);
CardCollection creaturesToGet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard),
CardPredicates.CREATURES
@@ -1454,7 +1516,7 @@ public class SpecialCardAi {
CardLists.sortByCmcDesc(creaturesToGet);
if (creaturesToGet.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// pick the best creature that will stay on the battlefield
@@ -1469,10 +1531,11 @@ public class SpecialCardAi {
if (best != null) {
sa.resetTargets();
sa.getTargets().add(best);
return true;
sa.setXManaCostPaid(best.getCMC());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
@@ -1586,23 +1649,27 @@ public class SpecialCardAi {
// The One Ring
public static class TheOneRing {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
if (!ai.canLoseLife() || ai.cantLoseForZeroOrLessLife()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
int lifeInDanger = aic.getIntProperty(AiProps.AI_IN_DANGER_THRESHOLD);
int numCtrs = sa.getHostCard().getCounters(CounterEnumType.BURDEN);
return ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1;
if (ai.getLife() > numCtrs + 1 && ai.getLife() > lifeInDanger
&& ai.getMaxHandSize() >= ai.getCardsIn(ZoneType.Hand).size() + numCtrs + 1) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.LifeInDanger);
}
}
// The Scarab God
public static class TheScarabGod {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
Card bestOppCreat = ComputerUtilCard.getBestAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
Card worstOwnCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.CREATURES));
@@ -1613,13 +1680,19 @@ public class SpecialCardAi {
sa.getTargets().add(worstOwnCreat);
}
return sa.getTargets().size() > 0;
if (!sa.getTargets().isEmpty()) {
// If we have a target, we can play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// No valid targets, can't play this ability
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
}
// Timetwister
public static class Timetwister {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
int maxOppHandSize = 0;
@@ -1633,7 +1706,14 @@ public class SpecialCardAi {
}
// use in case we're getting low on cards or if we're significantly behind our opponent in cards in hand
return aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD;
if (aiHandSize < HAND_SIZE_THRESHOLD || maxOppHandSize - aiHandSize > HAND_SIZE_THRESHOLD) {
// if the AI has less than 3 cards in hand or the opponent has more than 3 cards in hand than the AI
// then the AI is willing to play this ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// otherwise, don't play this ability
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -1694,12 +1774,12 @@ public class SpecialCardAi {
// Volrath's Shapeshifter
public static class VolrathsShapeshifter {
public static boolean consider(final Player ai, final SpellAbility sa) {
public static AiAbilityDecision consider(final Player ai, final SpellAbility sa) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN)) {
// try not to do this too early to at least attempt to avoid situations where the AI
// would cast a spell which would ruin the shapeshifting
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
@@ -1715,11 +1795,15 @@ public class SpecialCardAi {
if (topGY == null
|| !topGY.isCreature()
|| ComputerUtilCard.evaluateCreature(creatHand) > ComputerUtilCard.evaluateCreature(topGY) + 80) {
return numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false);
if ( numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) {

View File

@@ -39,70 +39,75 @@ import forge.util.collect.FCollectionView;
*/
public abstract class SpellAbilityAi {
public final boolean canPlayAIWithSubs(final Player aiPlayer, final SpellAbility sa) {
if (!canPlayAI(aiPlayer, sa)) {
return false;
public final AiAbilityDecision canPlayWithSubs(final Player aiPlayer, final SpellAbility sa) {
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (!decision.willingToPlay() && !"PlayForSub".equals(sa.getParam("AILogic"))) {
return decision;
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb);
if (subAb == null) {
return decision;
}
return chkDrawbackWithSubs(aiPlayer, subAb);
}
/**
* Handles the AI decision to play a "main" SpellAbility
*/
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(source, sa)) {
return false;
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
if (sa.getRestrictions() != null && !sa.getRestrictions().canPlay(sa.getHostCard(), sa)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
return canPlayWithoutRestrict(ai, sa);
}
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final Cost cost = sa.getPayCosts();
if (sa.hasParam("AICheckCanPlayWithDefinedX")) {
// FIXME: can this somehow be simplified without the need for an extra AI hint?
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
}
if (!checkConditions(ai, sa, sa.getConditions())) {
SpellAbility sub = sa.getSubAbility();
if (sub != null && !checkConditions(ai, sub, sub.getConditions())) {
return false;
}
}
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
final boolean alwaysOnDiscard = "AlwaysOnDiscard".equals(logic) && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN, ai)
&& !ai.isUnlimitedHandSize() && ai.getCardsIn(ZoneType.Hand).size() > ai.getMaxHandSize();
if (!checkAiLogic(ai, sa, logic)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (!alwaysOnDiscard && !checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler(), logic)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
}
} else if (!checkPhaseRestrictions(ai, sa, ai.getGame().getPhaseHandler())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingPhaseRestrictions);
} else if (ComputerUtil.preventRunAwayActivations(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
}
if (!checkApiLogic(ai, sa)) {
return false;
AiAbilityDecision decision = checkApiLogic(ai, sa);
if (!decision.willingToPlay()) {
return decision;
}
// needs to be after API logic because needs to check possible X Cost?
// needs to be after API logic because needs to check possible X Cost
if (cost != null && !willPayCosts(ai, sa, cost, source)) {
return false;
}
return true;
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
}
protected boolean checkConditions(final Player ai, final SpellAbility sa, SpellAbilityCondition con) {
// for cards like Figure of Destiny
// (it's unlikely many valid effect would work like this -
// and while in theory AI could turn some conditions true in response that's far too advanced as default)
if (!checkConditions(ai, sa)) {
SpellAbility sub = sa.getSubAbility();
if (sub == null || !checkConditions(ai, sub)) {
return new AiAbilityDecision(0, AiPlayDecision.NeedsToPlayCriteriaNotMet);
}
}
return decision;
}
protected boolean checkConditions(final Player ai, final SpellAbility sa) {
// copy it to disable some checks that the AI need to check extra
con = (SpellAbilityCondition) con.copy();
SpellAbilityCondition con = (SpellAbilityCondition) sa.getConditions().copy();
// if manaspent, check if AI can pay the colored mana as cost
if (!con.getManaSpent().isEmpty()) {
@@ -116,40 +121,6 @@ public abstract class SpellAbilityAi {
return con.areMet(sa);
}
/**
* Checks if the AI will play a SpellAbility with the specified AiLogic
*/
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (aiLogic.equals("CheckCondition")) {
SpellAbility saCopy = sa.copy();
saCopy.setActivatingPlayer(ai);
return saCopy.metConditions();
}
return !("Never".equals(aiLogic));
}
/**
* Checks if the AI is willing to pay for additional costs
* <p>
* Evaluated costs are: life, discard, sacrifice and counter-removal
*/
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return false;
}
return true;
}
/**
* Checks if the AI will play a SpellAbility based on its phase restrictions
*/
@@ -159,19 +130,38 @@ public abstract class SpellAbilityAi {
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
final String logic) {
if (logic.equals("AtOppEOT")) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
return checkPhaseRestrictions(ai, sa, ph);
}
/**
* Checks if the AI will play a SpellAbility with the specified AiLogic
*/
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if ("Never".equals(aiLogic)) {
return false;
}
if (!"Once".equals(aiLogic)) {
return !sa.getHostCard().getAbilityActivatedThisTurn().getActivators(sa).contains(ai);
}
return true;
}
/**
* The rest of the logic not covered by the canPlayAI template is defined here
*/
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop
}
return MyRandom.getRandom().nextFloat() < .8f; // random success
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.getActivationsThisTurn() == 0 || MyRandom.getRandom().nextFloat() < .8f) {
// 80% chance to play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
public final boolean doTriggerAI(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
public final boolean doTrigger(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
// this evaluation order is currently intentional as it does more stuff that helps avoiding some crashes
if (!ComputerUtilCost.canPayCost(sa, aiPlayer, true) && !mandatory) {
return false;
@@ -183,28 +173,48 @@ public abstract class SpellAbilityAi {
return sa.isTargetNumberValid();
}
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory);
return doTriggerNoCostWithSubs(aiPlayer, sa, mandatory).willingToPlay();
}
public final boolean doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (!doTriggerAINoCost(aiPlayer, sa, mandatory) && !"Always".equals(sa.getParam("AILogic"))) {
return false;
public final AiAbilityDecision doTriggerNoCostWithSubs(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
AiAbilityDecision decision = doTriggerNoCost(aiPlayer, sa, mandatory);
if (!decision.willingToPlay() && !"Always".equals(sa.getParam("AILogic"))) {
return decision;
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || chkDrawbackWithSubs(aiPlayer, subAb) || mandatory;
if (subAb == null) {
if (decision.willingToPlay()) {
return decision;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
decision = chkDrawbackWithSubs(aiPlayer, subAb);
if (decision.willingToPlay()) {
return decision;
}
if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/**
* Handles the AI decision to play a triggered SpellAbility
*/
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (canPlayWithoutRestrict(aiPlayer, sa) && (!mandatory || sa.isTargetNumberValid())) {
return true;
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
AiAbilityDecision decision = canPlayWithoutRestrict(aiPlayer, sa);
if (decision.willingToPlay() && (!mandatory || sa.isTargetNumberValid())) {
// This is a weird check. Why do we care if its not mandatory if we WANT to do it?
return decision;
}
// not mandatory, short way out
if (!mandatory) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// invalid target might prevent it
@@ -220,82 +230,13 @@ public abstract class SpellAbilityAi {
if (sa.canTarget(p)) {
sa.resetTargets();
sa.getTargets().add(p);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return true;
}
/**
* Handles the AI decision to play a sub-SpellAbility
*/
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
// sub-SpellAbility might use targets too
if (sa.usesTargeting()) {
// no Candidates, no adding to Stack
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
return false;
}
// but if it does, it should override this function
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return false;
}
return true;
}
/**
* <p>
* isSorcerySpeed.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Adventure).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
}
/**
* <p>
* playReusable.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
PhaseHandler phase = ai.getGame().getPhaseHandler();
// TODO probably also consider if winter orb or similar are out
if (sa instanceof AbilitySub) {
return true; // This is only true for Drawbacks and triggers
}
if (!sa.getPayCosts().isReusuableResource()) {
return false;
}
if (ComputerUtil.playImmediately(ai, sa)) {
return true;
}
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
return true;
}
if (sa.isSpell() && !sa.isBuyback()) {
return false;
}
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -304,9 +245,35 @@ public abstract class SpellAbilityAi {
* @param ab
* @return
*/
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
final AbilitySub subAb = ab.getSubAbility();
return SpellApiToAi.Converter.get(ab).chkAIDrawback(ab, aiPlayer) && (subAb == null || chkDrawbackWithSubs(aiPlayer, subAb));
AiAbilityDecision decision = SpellApiToAi.Converter.get(ab).chkDrawback(ab, aiPlayer);
if (!decision.willingToPlay()) {
return decision;
}
if (subAb == null) {
return decision;
}
return chkDrawbackWithSubs(aiPlayer, subAb);
}
/**
* Handles the AI decision to play a sub-SpellAbility
*/
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
// sub-SpellAbility might use targets too
if (sa.usesTargeting()) {
// no Candidates, no adding to Stack
if (!sa.getTargetRestrictions().hasCandidates(sa)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// but if it does, it should override this function
System.err.println("Warning: default (ie. inherited from base class) implementation of chkAIDrawback is used by " + sa.getHostCard().getName() + " for " + this.getClass().getName() + ". Consider declaring an overloaded method");
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
@@ -314,25 +281,6 @@ public abstract class SpellAbilityAi {
return true;
}
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payNever = "Never".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && isMine)) {
return false;
}
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
}
@SuppressWarnings("unchecked")
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
boolean hasPlayer = false;
@@ -412,6 +360,46 @@ public abstract class SpellAbilityAi {
return MyRandom.getRandom().nextBoolean();
}
/**
* Checks if the AI is willing to pay for additional costs
* <p>
* Evaluated costs are: life, discard, sacrifice and counter-removal
*/
protected boolean willPayCosts(final Player ai, final SpellAbility sa, final Cost cost, final Card source) {
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return false;
}
return true;
}
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
final Card source = sa.getHostCard();
final String aiLogic = sa.getParam("UnlessAI");
boolean payNever = "Never".equals(aiLogic);
boolean isMine = sa.getActivatingPlayer().equals(payer);
if (payNever) { return false; }
// AI will only pay when it's not already payed and only opponents abilities
if (alreadyPaid || (payers.size() > 1 && isMine)) {
return false;
}
return ComputerUtilCost.checkLifeCost(payer, cost, source, 4, sa)
&& ComputerUtilCost.checkDamageCost(payer, cost, source, 4, sa)
&& (isMine || ComputerUtilCost.checkSacrificeCost(payer, cost, source, sa))
&& (isMine || ComputerUtilCost.checkDiscardCost(payer, cost, source, sa));
}
public List<OptionalCostValue> chooseOptionalCosts(SpellAbility chosen, Player player, List<OptionalCostValue> optionalCostValues) {
List<OptionalCostValue> chosenOptCosts = Lists.newArrayList();
Cost costSoFar = chosen.getPayCosts().copy();
@@ -421,14 +409,14 @@ public abstract class SpellAbilityAi {
Cost fullCost = opt.getCost().copy().add(costSoFar);
SpellAbility fullCostSa = chosen.copyWithDefinedCost(fullCost);
// Playability check for Kicker
if (opt.getType() == OptionalCost.Kicker1 || opt.getType() == OptionalCost.Kicker2) {
SpellAbility kickedSaCopy = fullCostSa.copy();
kickedSaCopy.addOptionalCost(opt.getType());
Card copy = CardCopyService.getLKICopy(chosen.getHostCard());
copy.setCastSA(kickedSaCopy);
if (ComputerUtilCard.checkNeedsToPlayReqs(copy, kickedSaCopy) != AiPlayDecision.WillPlay) {
continue; // don't choose kickers we don't want to play
// don't choose kickers we don't want to play
continue;
}
}
@@ -440,4 +428,56 @@ public abstract class SpellAbilityAi {
return chosenOptCosts;
}
/**
* <p>
* isSorcerySpeed.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean isSorcerySpeed(final SpellAbility sa, Player ai) {
return (sa.getRootAbility().isSpell() && sa.getHostCard().isSorcery())
|| (sa.getRootAbility().isActivatedAbility() && sa.getRootAbility().getRestrictions().isSorcerySpeed())
|| (sa.getRootAbility().isAdventure() && sa.getHostCard().getState(CardStateName.Secondary).getType().isSorcery())
|| (sa.isPwAbility() && !sa.withFlash(sa.getHostCard(), ai));
}
/**
* <p>
* playReusable.
* </p>
*
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
protected static boolean playReusable(final Player ai, final SpellAbility sa) {
PhaseHandler phase = ai.getGame().getPhaseHandler();
// TODO probably also consider if winter orb or similar are out
if (sa instanceof AbilitySub) {
return true; // This is only true for Drawbacks and triggers
}
if (!sa.getPayCosts().isReusuableResource()) {
return false;
}
if (ComputerUtil.playImmediately(ai, sa)) {
return true;
}
if (sa.isPwAbility() && phase.is(PhaseType.MAIN2)) {
return true;
}
if (sa.isSpell() && !sa.isBuyback()) {
return false;
}
return phase.is(PhaseType.END_OF_TURN) && phase.getNextTurn().equals(ai);
}
}

View File

@@ -88,6 +88,7 @@ public enum SpellApiToAi {
.put(ApiType.EachDamage, DamageEachAi.class)
.put(ApiType.Effect, EffectAi.class)
.put(ApiType.Encode, EncodeAi.class)
.put(ApiType.Endure, EndureAi.class)
.put(ApiType.EndCombatPhase, EndTurnAi.class)
.put(ApiType.EndTurn, EndTurnAi.class)
.put(ApiType.ExchangeLife, LifeExchangeAi.class)

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -8,7 +10,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
@@ -16,78 +17,69 @@ import java.util.Map;
public class ActivateAbilityAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
// AI cannot use this properly until he can use SAs during Humans turn
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Player opp = ai.getStrongestOpponent();
List<Card> list = CardLists.getType(opp.getCardsIn(ZoneType.Battlefield), sa.getParamOrDefault("Type", "Card"));
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
if (!sa.usesTargeting()) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (!defined.contains(opp)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
} else {
sa.resetTargets();
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return randomReturn;
return super.checkApiLogic(ai, sa);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getStrongestOpponent();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard();
if (null == tgt) {
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
return defined.contains(opp);
if (defined.contains(opp)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
sa.resetTargets();
sa.getTargets().add(opp);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
// AI cannot use this properly until he can use SAs during Humans turn
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard();
boolean randomReturn = true;
if (!sa.usesTargeting()) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
sa.resetTargets();
sa.getTargets().add(ai.getWeakestOpponent());
}
return randomReturn;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -11,8 +13,8 @@ import forge.game.spellability.SpellAbility;
public class AddPhaseAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false;
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -17,6 +17,8 @@
*/
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
@@ -38,7 +40,7 @@ import java.util.List;
public class AddTurnAi extends SpellAbilityAi {
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
@@ -56,32 +58,32 @@ public class AddTurnAi extends SpellAbilityAi {
if (!sa.getTargetRestrictions().isMinTargetsChosen(sa.getHostCard(), sa) && opp != null) {
sa.getTargets().add(opp);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
final List<Player> tgtPlayers = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
for (final Player p : tgtPlayers) {
if (p.isOpponentOf(ai) && !mandatory) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// TODO: improve ai for Sage of Hours
return StringUtils.isNumeric(sa.getParam("NumTurns"));
// not sure if the AI should be playing with cards that give the
// Human more turns.
if (!StringUtils.isNumeric(sa.getParam("NumTurns"))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return doTriggerAINoCost(aiPlayer, sa, false);
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return doTriggerNoCost(aiPlayer, sa, false);
}
}

View File

@@ -1,30 +1,23 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class AdvanceCrankAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
int nextSprocket = (ai.getCrankCounter() % 3) + 1;
int crankCount = CardLists.count(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.isContraptionOnSprocket(nextSprocket));
//Could evaluate whether we actually want to crank those, but this is probably fine for now.
if(crankCount < 2)
return false;
return super.canPlayAI(ai, sa);
if (crankCount < 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return super.canPlay(ai, sa);
}
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if(logic.equals("AtOppEOT"))
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -16,7 +18,7 @@ import java.util.Map;
public class AlterAttributeAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
boolean activate = Boolean.parseBoolean(sa.getParamOrDefault("Activate", "true"));
String[] attributes = sa.getParam("Attributes").split(",");
@@ -24,7 +26,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
// TODO add targeting logic
// needed for Suspected
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
@@ -36,7 +38,7 @@ public class AlterAttributeAi extends SpellAbilityAi {
case "Solved":
// there is currently no effect that would un-solve something
if (!c.isSolved() && activate) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
break;
case "Suspect":
@@ -44,21 +46,21 @@ public class AlterAttributeAi extends SpellAbilityAi {
// is Suspected good or bad?
// currently Suspected is better
if (!activate) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
case "Saddle":
case "Saddled":
// AI should not try to Saddle again?
if (c.isSaddled()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
@@ -13,8 +14,8 @@ public class AlwaysPlayAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override

View File

@@ -3,6 +3,8 @@ package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -19,24 +21,28 @@ import java.util.Map;
public class AmassAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
CardCollection aiArmies = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "Army");
Card host = sa.getHostCard();
final Game game = ai.getGame();
if (!aiArmies.isEmpty()) {
return aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1));
if (aiArmies.anyMatch(CardPredicates.canReceiveCounters(CounterEnumType.P1P1))) {
// If AI has an Army that can receive counters, play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// AI has Armies but none can receive counters, so don't play
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactGame);
}
}
final String type = sa.getParam("Type");
StringBuilder sb = new StringBuilder("b_0_0_");
sb.append(sa.getOriginalParam("Type").toLowerCase()).append("_army");
final String tokenScript = sb.toString();
final String tokenScript = "b_0_0_" + sa.getOriginalParam("Type").toLowerCase() + "_army";
final int amount = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("Num", "1"), sa);
Card token = TokenInfo.getProtoType(tokenScript, sa, ai, false);
if (token == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
token.setController(ai, 0);
@@ -63,7 +69,11 @@ public class AmassAi extends SpellAbilityAi {
//reset static abilities
game.getAction().checkStaticAbilities(false);
return result;
if (result) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@Override
@@ -82,8 +92,12 @@ public class AmassAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || checkApiLogic(ai, sa);
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return checkApiLogic(ai, sa);
}
@Override

View File

@@ -24,6 +24,7 @@ import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityContinuous;
import forge.game.staticability.StaticAbilityLayer;
import forge.game.staticability.StaticAbilityMode;
import forge.game.zone.ZoneType;
import forge.util.FileSection;
import forge.util.collect.FCollectionView;
@@ -141,31 +142,32 @@ public class AnimateAi extends SpellAbilityAi {
}
@Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = aiPlayer.getGame();
final PhaseHandler ph = game.getPhaseHandler();
if (!sa.metConditions() && sa.getSubAbility() == null) {
return false; // what is this for?
}
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getApi() == ApiType.Sacrifice) {
// Should I animate a card before i have to sacrifice something better?
if (!isAnimatedThisTurn(aiPlayer, source)) {
rememberAnimatedThisTurn(aiPlayer, source);
return true; // interrupt sacrifice
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
}
}
if (!ComputerUtilCost.checkTapTypeCost(aiPlayer, sa.getPayCosts(), source, sa, new CardCollection())) {
return false; // prevent crewing with equal or better creatures
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
}
if (sa.costHasManaX() && sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
final int xPay = ComputerUtilCost.getMaxXValue(sa, aiPlayer, sa.isTrigger());
sa.setXManaCostPaid(xPay);
}
if (!sa.usesTargeting()) {
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
}
final List<Card> defined = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
boolean bFlag = false;
boolean givesHaste = sa.hasParam("Keywords") && sa.getParam("Keywords").contains("Haste");
@@ -201,22 +203,22 @@ public class AnimateAi extends SpellAbilityAi {
if (!isSorcerySpeed(sa, aiPlayer) && !"Permanent".equals(sa.getParam("Duration"))) {
if (sa.isCrew() && c.isCreature()) {
// Do not try to crew a vehicle which is already a creature
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card animatedCopy = becomeAnimated(c, sa);
if (ph.isPlayerTurn(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureAttackAI(aiPlayer, animatedCopy)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
if (ph.getPlayerTurn().isOpponentOf(aiPlayer)
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.DoesntImpactCombat);
}
// also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
if (!isAnimatedThisTurn(aiPlayer, source)) {
if (!c.isTapped() || (ph.inCombat() && game.getCombat().isAttacking(c))) {
bFlag = true;
}
@@ -225,46 +227,44 @@ public class AnimateAi extends SpellAbilityAi {
}
}
if (bFlag) {
rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
rememberAnimatedThisTurn(aiPlayer, source);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return bFlag; // All of the defined stuff is animated, not very useful
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
AiAbilityDecision decision;
if (sa.usesTargeting()) {
decision = animateTgtAI(sa);
if (decision.willingToPlay()) {
return decision;
} else if (!mandatory) {
return decision;
} else {
sa.resetTargets();
return animateTgtAI(sa);
}
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.usesTargeting()) {
sa.resetTargets();
return animateTgtAI(sa);
}
return true;
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
if(animateTgtAI(sa))
return true;
else if (!mandatory)
return false;
else {
// fallback if animate is mandatory
sa.resetTargets();
List<Card> list = CardUtil.getValidCardsToTarget(sa);
if (list.isEmpty()) {
return false;
return decision;
}
Card toAnimate = ComputerUtilCard.getWorstAI(list);
rememberAnimatedThisTurn(aiPlayer, toAnimate);
sa.getTargets().add(toAnimate);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
@@ -272,9 +272,14 @@ public class AnimateAi extends SpellAbilityAi {
return player.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2);
}
private boolean animateTgtAI(final SpellAbility sa) {
private AiAbilityDecision animateTgtAI(final SpellAbility sa) {
if (sa.getMaxTargets() == 0) {
// this happens if an optional cost is skipped, e.g. Brave the Wilds
return new AiAbilityDecision(80, AiPlayDecision.WillPlay);
}
final Player ai = sa.getActivatingPlayer();
final PhaseHandler ph = ai.getGame().getPhaseHandler();
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean alwaysActivatePWAbility = sa.isPwAbility()
&& sa.getPayCosts().hasSpecificCostType(CostPutCounter.class)
@@ -286,15 +291,13 @@ public class AnimateAi extends SpellAbilityAi {
types.addAll(Arrays.asList(sa.getParam("Types").split(",")));
}
final Game game = ai.getGame();
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, false);
// list is empty, no possible targets
if (list.isEmpty() && !alwaysActivatePWAbility) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// something is used for animate into creature
@@ -361,7 +364,7 @@ public class AnimateAi extends SpellAbilityAi {
// data is empty, no good targets
if (data.isEmpty() && !alwaysActivatePWAbility) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// get the best creature to be animated
@@ -384,17 +387,18 @@ public class AnimateAi extends SpellAbilityAi {
holdAnimatedTillMain2(ai, worst);
if (!ComputerUtilMana.canPayManaCost(sa, ai, 0, sa.isTrigger())) {
releaseHeldTillMain2(ai, worst);
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
}
rememberAnimatedThisTurn(ai, worst);
sa.getTargets().add(worst);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (logic.equals("SetPT")) {
// TODO: 1. Teach the AI to use this to save the creature from direct damage; 2. Determine the best target in a smarter way?
// TODO: 1. Teach the AI to use this to save the creature from direct damage;
// 2. Determine the best target in a smarter way?
Card worst = ComputerUtilCard.getWorstCreatureAI(ai.getCreaturesInPlay());
Card buffed = becomeAnimated(worst, sa);
@@ -402,7 +406,7 @@ public class AnimateAi extends SpellAbilityAi {
&& (buffed.getNetPower() - worst.getNetPower() >= 3 || !ComputerUtilCard.doesCreatureAttackAI(ai, worst))) {
sa.getTargets().add(worst);
rememberAnimatedThisTurn(ai, worst);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -414,7 +418,7 @@ public class AnimateAi extends SpellAbilityAi {
boolean isValuableAttacker = ph.is(PhaseType.MAIN1, ai) && ComputerUtilCard.doesSpecifiedCreatureAttackAI(ai, animated);
boolean isValuableBlocker = combat != null && combat.getDefendingPlayers().contains(ai) && ComputerUtilCard.doesSpecifiedCreatureBlock(ai, animated);
if (isValuableAttacker || isValuableBlocker)
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
@@ -424,25 +428,23 @@ public class AnimateAi extends SpellAbilityAi {
if(worst != null) {
sa.getTargets().add(worst);
rememberAnimatedThisTurn(ai, worst);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
if (sa.hasParam("AITgts") && !list.isEmpty()) {
//No logic, but we do have preferences. Pick the best among those?
Card best = ComputerUtilCard.getBestAI(list);
if(best != null) {
sa.getTargets().add(best);
rememberAnimatedThisTurn(ai, best);
return true;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things
// that animate a target. Those can just use AI:RemoveDeck:All until
// this can do a reasonably good job of picking a good target
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
public static Card becomeAnimated(final Card card, final SpellAbility sa) {
@@ -562,7 +564,7 @@ public class AnimateAi extends SpellAbilityAi {
CardTraitChanges traits = card.getChangedCardTraits().get(timestamp, 0);
if (traits != null) {
for (StaticAbility stAb : traits.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
for (final StaticAbilityLayer layer : stAb.getLayers()) {
StaticAbilityContinuous.applyContinuousAbility(stAb, new CardCollection(card), layer);
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -9,24 +11,30 @@ import forge.game.spellability.SpellAbility;
public class AnimateAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
String logic = sa.getParamOrDefault("AILogic", "");
if ("CreatureAdvantage".equals(logic) && !aiPlayer.getCreaturesInPlay().isEmpty()) {
// TODO: improve this or implement a better logic for abilities like Oko, the Trickster ultimate
for (Card c : aiPlayer.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(aiPlayer, c)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
return "Always".equals(logic);
} // end animateAllCanPlayAI()
if ("Always".equals(logic)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
}
}

View File

@@ -1,13 +1,13 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
@@ -16,30 +16,32 @@ import java.util.List;
public class AssembleContraptionAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
//Pulls double duty as the OpenAttraction API. Same logic; usually good to do as long as we have the appropriate cards.
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
CardCollectionView deck = getDeck(ai, sa);
if(deck.isEmpty())
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
if(!super.canPlayAI(ai, sa))
return false;
AiAbilityDecision superDecision = super.canPlay(ai, sa);
if (!superDecision.willingToPlay())
return superDecision;
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
xPay = Math.max(xPay, deck.size());
if (xPay == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
sa.getRootAbility().setXManaCostPaid(xPay);
}
if(sa.hasParam("DefinedContraption") && sa.usesTargeting()) {
return getGoodReassembleTarget(ai, sa) != null;
if (getGoodReassembleTarget(ai, sa) == null) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private static CardCollectionView getDeck(Player ai, SpellAbility sa) {
@@ -48,11 +50,11 @@ public class AssembleContraptionAi extends SpellAbilityAi {
}
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if ("X".equals(sa.getParam("Amount")) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getRootAbility().setXManaCostPaid(xPay);
}
@@ -62,7 +64,7 @@ public class AssembleContraptionAi extends SpellAbilityAi {
if(target != null)
sa.getTargets().add(target);
else
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return super.checkApiLogic(ai, sa);
@@ -84,26 +86,16 @@ public class AssembleContraptionAi extends SpellAbilityAi {
}
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if(logic.equals("AtOppEOT"))
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
return super.checkPhaseRestrictions(ai, sa, ph);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
if(getDeck(aiPlayer, sa).isEmpty())
return false;
return super.chkAIDrawback(sa, aiPlayer);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if(!mandatory && getDeck(aiPlayer, sa).isEmpty())
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
}
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
@Override
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
if(getDeck(aiPlayer, sa).isEmpty())
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
return super.chkDrawback(sa, aiPlayer);
}
}

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -10,11 +12,11 @@ import java.util.Map;
public class AssignGroupAi extends SpellAbilityAi {
protected boolean canPlayAI(Player ai, SpellAbility sa) {
@Override
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
// TODO: Currently this AI relies on the card-specific limiting hints (NeedsToPlay / NeedsToPlayVar),
// otherwise the AI considers the card playable.
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells, Map<String, Object> params) {

View File

@@ -15,20 +15,23 @@ import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice;
import forge.game.keyword.Keyword;
import forge.game.keyword.KeywordInterface;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplacementLayer;
import forge.game.replacement.ReplacementType;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility;
import forge.game.staticability.StaticAbilityCantAttackBlock;
import forge.game.staticability.StaticAbilityMode;
import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import forge.util.MyRandom;
import org.apache.commons.lang3.ObjectUtils;
import java.util.ArrayList;
import java.util.Arrays;
@@ -42,24 +45,14 @@ public class AttachAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
// TODO: improve this so that the AI can use a flash aura buff as a means of killing opposing creatures
// and gaining card advantage
if (source.hasKeyword("MayFlashSac") && !ai.canCastSorcery()) {
return false;
}
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
}
if (source.isAura() && sa.isSpell() && !source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
@@ -67,20 +60,16 @@ public class AttachAi extends SpellAbilityAi {
// TODO: Add some extra checks for where the AI may want to cast a replacement aura
// on another creature and keep it when the original enchanted creature is useless
return false;
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
}
// Attach spells always have a target
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
sa.resetTargets();
if (!attachPreference(sa, tgt, false)) {
return false;
AiAbilityDecision attachDecision = attachPreference(sa, tgt, false);
if (!attachDecision.willingToPlay()) {
return attachDecision;
}
}
@@ -91,7 +80,7 @@ public class AttachAi extends SpellAbilityAi {
}
if ((source.hasKeyword(Keyword.FLASH) || (!ai.canCastSorcery() && sa.canCastTiming(ai)))
&& source.isAura() && advancedFlash && !doAdvancedFlashAuraLogic(ai, sa, sa.getTargetCard())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (abCost.getTotalMana().countX() > 0 && sa.getSVar("X").equals("Count$xPaid")) {
@@ -99,7 +88,7 @@ public class AttachAi extends SpellAbilityAi {
final int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
sa.setXManaCostPaid(xPay);
@@ -109,10 +98,10 @@ public class AttachAi extends SpellAbilityAi {
final SpellAbility effectExile = AbilityFactory.getAbility(source.getSVar("TrigExile"), source);
effectExile.setActivatingPlayer(ai);
final List<Card> targets = CardUtil.getValidCardsToTarget(effectExile);
return !targets.isEmpty();
return !targets.isEmpty() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private boolean doAdvancedFlashAuraLogic(Player ai, SpellAbility sa, Card attachTarget) {
@@ -130,7 +119,7 @@ public class AttachAi extends SpellAbilityAi {
int power = 0, toughness = 0;
List<String> keywords = Lists.newArrayList();
for (StaticAbility stAb : source.getStaticAbilities()) {
if ("Continuous".equals(stAb.getParam("Mode"))) {
if (stAb.checkMode(StaticAbilityMode.Continuous)) {
if (stAb.hasParam("AddPower")) {
power += AbilityUtils.calculateAmount(source, stAb.getParam("AddPower"), stAb);
}
@@ -307,9 +296,8 @@ public class AttachAi extends SpellAbilityAi {
String type = "";
for (final StaticAbility stAb : attachSource.getStaticAbilities()) {
final Map<String, String> stab = stAb.getMapParams();
if (stab.get("Mode").equals("Continuous") && stab.containsKey("AddType")) {
type = stab.get("AddType");
if (stAb.checkMode(StaticAbilityMode.Continuous) && stAb.hasParam("AddType")) {
type = stAb.getParam("AddType");
}
}
@@ -371,9 +359,39 @@ public class AttachAi extends SpellAbilityAi {
*/
private static Card attachAIKeepTappedPreference(final SpellAbility sa, final List<Card> list, final boolean mandatory, final Card attachSource) {
// AI For Cards like Paralyzing Grasp and Glimmerdust Nap
// check for ETB Trigger
boolean tapETB = isAuraSpell(sa) && attachSource.getTriggers().anyMatch(t -> {
if (t.getMode() != TriggerType.ChangesZone) {
return false;
}
if (!ZoneType.Battlefield.toString().equals(t.getParam("Destination"))) {
return false;
}
if (t.hasParam("ValidCard") && !t.getParam("ValidCard").contains("Self")) {
return false;
}
SpellAbility tSa = t.ensureAbility();
if (tSa == null) {
return false;
}
if (!ApiType.Tap.equals(tSa.getApi())) {
return false;
}
if (!"Enchanted".equals(tSa.getParam("Defined"))) {
return false;
}
return true;
});
final List<Card> prefList = CardLists.filter(list, c -> {
// Don't do Untapped Vigilance cards
if (c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
if (!tapETB && c.isCreature() && c.hasKeyword(Keyword.VIGILANCE) && c.isUntapped()) {
return false;
}
@@ -388,21 +406,10 @@ public class AttachAi extends SpellAbilityAi {
return false;
}
}
if (!c.isEnchanted()) {
return true;
}
final Iterable<Card> auras = c.getEnchantedBy();
for (Card aura : auras) {
SpellAbility auraSA = aura.getSpells().get(0);
if (auraSA.getApi() == ApiType.Attach) {
if ("KeepTapped".equals(auraSA.getParam("AILogic"))) {
// Don't attach multiple KeepTapped Auras to one card
// already affected
if (!c.canUntap(c.getController(), true)) {
return false;
}
}
}
return true;
});
@@ -549,28 +556,46 @@ public class AttachAi extends SpellAbilityAi {
final Card attachSource) {
// AI For choosing a Card to Animate.
final Player ai = sa.getActivatingPlayer();
final Card attachSourceLki = CardCopyService.getLKICopy(attachSource);
Card attachSourceLki = null;
for (Trigger t : attachSource.getTriggers()) {
if (!t.getMode().equals(TriggerType.ChangesZone)) {
continue;
}
if (!"Battlefield".equals(t.getParam("Destination"))) {
continue;
}
if (!"Card.Self".equals(t.getParam("ValidCard"))) {
continue;
}
SpellAbility trigSa = t.ensureAbility();
SpellAbility animateSa = trigSa.findSubAbilityByType(ApiType.Animate);
if (animateSa == null) {
continue;
}
animateSa.setActivatingPlayer(sa.getActivatingPlayer());
attachSourceLki = AnimateAi.becomeAnimated(attachSource, animateSa);
}
if (attachSourceLki == null) {
return null;
}
attachSourceLki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
// Suppress original attach Spell to replace it with another
attachSourceLki.getFirstAttachSpell().setSuppressed(true);
final Card finalAttachSourceLki = attachSourceLki;
//TODO for Reanimate Auras i need the new Attach Spell, in later versions it might be part of the Enchant Keyword
attachSourceLki.addSpellAbility(AbilityFactory.getAbility(attachSourceLki, "NewAttach"));
List<Card> betterList = CardLists.filter(list, c -> {
final Card lki = CardCopyService.getLKICopy(c);
// need to fake it as if lki would be on the battlefield
lki.setLastKnownZone(ai.getZone(ZoneType.Battlefield));
// Reanimate Auras use "Enchant creature put onto the battlefield with CARDNAME" with Remembered
attachSourceLki.clearRemembered();
attachSourceLki.addRemembered(lki);
finalAttachSourceLki.clearRemembered();
finalAttachSourceLki.addRemembered(lki);
// need to check what the cards would be on the battlefield
// do not attach yet, that would cause Events
CardCollection preList = new CardCollection(lki);
preList.add(attachSourceLki);
preList.add(finalAttachSourceLki);
c.getGame().getAction().checkStaticAbilities(false, Sets.newHashSet(preList), preList);
boolean result = lki.canBeAttached(attachSourceLki, null);
boolean result = lki.canBeAttached(finalAttachSourceLki, null);
//reset static abilities
c.getGame().getAction().checkStaticAbilities(false);
@@ -795,27 +820,45 @@ public class AttachAi extends SpellAbilityAi {
int totPower = 0;
final List<String> keywords = new ArrayList<>();
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams();
boolean cantAttack = false;
boolean cantBlock = false;
if (!stabMap.get("Mode").equals("Continuous")) {
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
if (stAbility.checkMode(StaticAbilityMode.CantAttack)) {
String valid = stAbility.getParam("ValidCard");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantAttack = true;
}
} else if (stAbility.checkMode(StaticAbilityMode.CantBlock)) {
String valid = stAbility.getParam("ValidCard");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantBlock = true;
}
} else if (stAbility.checkMode(StaticAbilityMode.CantBlockBy)) {
String valid = stAbility.getParam("ValidBlocker");
if (valid.contains(stCheck) || valid.contains("AttachedBy")) {
cantBlock = true;
}
}
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
final String affected = stabMap.get("Affected");
final String affected = stAbility.getParam("Affected");
if (affected == null) {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), sa);
String kws = stabMap.get("AddKeyword");
String kws = stAbility.getParam("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
kws = stabMap.get("AddHiddenKeyword");
kws = stAbility.getParam("AddHiddenKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
@@ -851,6 +894,12 @@ public class AttachAi extends SpellAbilityAi {
prefList = CardLists.filter(prefList, c -> c.getNetPower() > 0 && ComputerUtilCombat.canAttackNextTurn(c));
}
if (cantAttack) {
prefList = CardLists.filter(prefList, c -> c.isCreature() && ComputerUtilCombat.canAttackNextTurn(c));
} else if (cantBlock) { // TODO better can block filter?
prefList = CardLists.filter(prefList, c -> c.isCreature() && !ComputerUtilCard.isUselessCreature(ai, c));
}
//some auras aren't useful in multiples
if (attachSource.hasSVar("NonStackingAttachEffect")) {
prefList = CardLists.filter(prefList,
@@ -892,9 +941,8 @@ public class AttachAi extends SpellAbilityAi {
* @return true, if successful
*/
@Override
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
final Card card = sa.getHostCard();
// Check if there are any valid targets
List<GameObject> targets = new ArrayList<>();
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
@@ -906,23 +954,48 @@ public class AttachAi extends SpellAbilityAi {
if (!mandatory && card.isEquipment() && !targets.isEmpty()) {
Card newTarget = (Card) targets.get(0);
//don't equip human creatures
if (newTarget.getController().isOpponentOf(ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
//don't equip a worse creature
if (card.isEquipping()) {
Card oldTarget = card.getEquipping();
if (ComputerUtilCard.evaluateCreature(oldTarget) > ComputerUtilCard.evaluateCreature(newTarget)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// don't equip creatures that don't gain anything
return !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
boolean stacking = !card.hasSVar("NonStackingAttachEffect") || !newTarget.isEquippedBy(card.getName());
if (!stacking) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return true;
@Override
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player ai) {
if (sa.isTrigger() && sa.usesTargeting()) {
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
if (tgt != null) {
sa.resetTargets();
sa.getTargets().add(tgt);
}
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
private static boolean isAuraSpell(final SpellAbility sa) {
return sa.isSpell() && sa.getHostCard().isAura();
}
/**
@@ -938,9 +1011,25 @@ public class AttachAi extends SpellAbilityAi {
* the mandatory
* @return true, if successful
*/
private static boolean attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
private static AiAbilityDecision attachPreference(final SpellAbility sa, final TargetRestrictions tgt, final boolean mandatory) {
GameObject o;
if (tgt.canTgtPlayer()) {
boolean spellCanTargetPlayer = false;
if (isAuraSpell(sa)) {
Card source = sa.getHostCard();
if (!source.hasKeyword(Keyword.ENCHANT)) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
for (KeywordInterface ki : source.getKeywords(Keyword.ENCHANT)) {
String ko = ki.getOriginal();
String m[] = ko.split(":");
String v = m[1];
if (v.contains("Player") || v.contains("Opponent")) {
spellCanTargetPlayer = true;
break;
}
}
}
if (tgt.canTgtPlayer() && (!isAuraSpell(sa) || spellCanTargetPlayer)) {
List<Player> targetable = new ArrayList<>();
for (final Player player : sa.getHostCard().getGame().getPlayers()) {
if (sa.canTarget(player)) {
@@ -953,11 +1042,11 @@ public class AttachAi extends SpellAbilityAi {
}
if (o == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
sa.getTargets().add(o);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -1005,9 +1094,8 @@ public class AttachAi extends SpellAbilityAi {
CardCollection toRemove = new CardCollection();
for (Trigger t : attachSource.getTriggers()) {
if (t.getMode() == TriggerType.ChangesZone) {
final Map<String, String> params = t.getMapParams();
if ("Card.Self".equals(params.get("ValidCard"))
&& "Battlefield".equals(params.get("Destination"))) {
if ("Card.Self".equals(t.getParam("ValidCard"))
&& "Battlefield".equals(t.getParam("Destination"))) {
SpellAbility trigSa = t.ensureAbility();
if (trigSa != null && trigSa.getApi() == ApiType.DealDamage && "Enchanted".equals(trigSa.getParam("Defined"))) {
for (Card target : list) {
@@ -1067,29 +1155,27 @@ public class AttachAi extends SpellAbilityAi {
boolean grantingExtraBlock = false;
for (final StaticAbility stAbility : attachSource.getStaticAbilities()) {
final Map<String, String> stabMap = stAbility.getMapParams();
if (!"Continuous".equals(stabMap.get("Mode"))) {
if (!stAbility.checkMode(StaticAbilityMode.Continuous)) {
continue;
}
final String affected = stabMap.get("Affected");
final String affected = stAbility.getParam("Affected");
if (affected == null) {
continue;
}
if (affected.contains(stCheck) || affected.contains("AttachedBy")) {
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), stAbility);
totToughness += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddToughness"), stAbility);
totPower += AbilityUtils.calculateAmount(attachSource, stAbility.getParam("AddPower"), stAbility);
grantingAbilities |= stabMap.containsKey("AddAbility");
grantingExtraBlock |= stabMap.containsKey("CanBlockAmount") || stabMap.containsKey("CanBlockAny");
grantingAbilities |= stAbility.hasParam("AddAbility");
grantingExtraBlock |= stAbility.hasParam("CanBlockAmount") || stAbility.hasParam("CanBlockAny");
String kws = stabMap.get("AddKeyword");
String kws = stAbility.getParam("AddKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
kws = stabMap.get("AddHiddenKeyword");
kws = stAbility.getParam("AddHiddenKeyword");
if (kws != null) {
keywords.addAll(Arrays.asList(kws.split(" & ")));
}
@@ -1158,12 +1244,17 @@ public class AttachAi extends SpellAbilityAi {
// TODO Somehow test for definitive advantage (e.g. opponent low on health, AI is attacking)
// to be able to deal the final blow with an enchanted vehicle like that
boolean canOnlyTargetCreatures = true;
for (String valid : ObjectUtils.firstNonNull(attachSource.getFirstAttachSpell(), sa).getTargetRestrictions().getValidTgts()) {
if (!valid.startsWith("Creature")) {
if (attachSource.isAura()) {
for (KeywordInterface ki : attachSource.getKeywords(Keyword.ENCHANT)) {
String o = ki.getOriginal();
String m[] = o.split(":");
String v = m[1];
if (!v.startsWith("Creature")) {
canOnlyTargetCreatures = false;
break;
}
}
}
if (canOnlyTargetCreatures && (attachSource.isAura() || attachSource.isEquipment())) {
prefList = CardLists.filter(prefList, c -> c.getTimesCrewedThisTurn() == 0 || (attachSource.isEquipment() && attachSource.getGame().getPhaseHandler().is(PhaseType.MAIN1, ai)));
}
@@ -1387,8 +1478,6 @@ public class AttachAi extends SpellAbilityAi {
c = attachAICuriosityPreference(sa, prefList, mandatory, attachSource);
} else if ("ChangeType".equals(logic)) {
c = attachAIChangeTypePreference(sa, prefList, mandatory, attachSource);
} else if ("KeepTapped".equals(logic)) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
} else if ("Animate".equals(logic)) {
c = attachAIAnimatePreference(sa, prefList, mandatory, attachSource);
} else if ("Reanimate".equals(logic)) {
@@ -1399,6 +1488,12 @@ public class AttachAi extends SpellAbilityAi {
c = attachAIHighestEvaluationPreference(prefList);
}
if (isAuraSpell(sa)) {
if (attachSource.getReplacementEffects().anyMatch(re -> re.getMode().equals(ReplacementType.Untap) && re.getLayer().equals(ReplacementLayer.CantHappen))) {
c = attachAIKeepTappedPreference(sa, prefList, mandatory, attachSource);
}
}
// Consider exceptional cases which break the normal evaluation rules
if (!isUsefulAttachAction(ai, c, sa)) {
return null;
@@ -1551,8 +1646,6 @@ public class AttachAi extends SpellAbilityAi {
} else if (keyword.endsWith("Prevent all combat damage that would be dealt to and dealt by CARDNAME.")
|| keyword.endsWith("Prevent all damage that would be dealt to and dealt by CARDNAME.")) {
return card.getNetCombatDamage() >= 2 && ComputerUtilCombat.canAttackNextTurn(card);
} else if (keyword.endsWith("CARDNAME doesn't untap during your untap step.")) {
return !card.isUntapped();
}
return true;
}
@@ -1605,25 +1698,6 @@ public class AttachAi extends SpellAbilityAi {
return chosen;
}
@Override
public boolean chkAIDrawback(final SpellAbility sa, final Player ai) {
// TODO for targeting optional Halvar trigger, needs to be coordinated with PumpAi to make it playable
if (sa.isTrigger() && sa.usesTargeting()) {
CardCollection targetables = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
CardCollection source = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Object"), sa);
Card tgt = attachGeneralAI(ai, sa, targetables, !sa.getRootAbility().isOptionalTrigger(), source.getFirst(), null);
if (tgt != null) {
sa.resetTargets();
sa.getTargets().add(tgt);
}
return sa.isTargetNumberValid();
} else if ("Remembered".equals(sa.getParam("Defined")) && sa.getParent() != null
&& sa.getParent().getApi() == ApiType.Token && sa.getParent().hasParam("RememberTokens")) {
// Living Weapon or similar
return true;
}
return false;
}
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
@@ -11,7 +13,7 @@ import forge.util.MyRandom;
public class BalanceAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
String logic = sa.getParam("AILogic");
int diff = 0;
Player opp = aiPlayer.getWeakestOpponent();
@@ -37,7 +39,7 @@ public class BalanceAi extends SpellAbilityAi {
if (diff < 0) {
// Don't sacrifice permanents even if opponent has a ton of cards in hand
return false;
return new AiAbilityDecision(0, forge.ai.AiPlayDecision.CantPlayAi);
}
final CardCollectionView humHand = opp.getCardsIn(ZoneType.Hand);
@@ -45,6 +47,7 @@ public class BalanceAi extends SpellAbilityAi {
diff += 0.5 * (humHand.size() - compHand.size());
// Larger differential == more chance to actually cast this spell
return diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
boolean willPlay = diff > 2 && MyRandom.getRandom().nextInt(100) < diff*10;
return new AiAbilityDecision(willPlay ? 100 : 0, willPlay ? forge.ai.AiPlayDecision.WillPlay : AiPlayDecision.StopRunawayActivations);
}
}

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -16,14 +17,14 @@ import forge.game.zone.ZoneType;
public class BecomesBlockedAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Game game = aiPlayer.getGame();
if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| !game.getPhaseHandler().getPlayerTurn().isOpponentOf(aiPlayer)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (tgt != null) {
@@ -36,35 +37,31 @@ public class BecomesBlockedAi extends SpellAbilityAi {
Card choice = null;
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
choice = ComputerUtilCard.getBestCreatureAI(list);
if (choice == null) { // can't find anything left
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
list.remove(choice);
sa.getTargets().add(choice);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
// TODO - implement AI
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
boolean chance;
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// TODO - implement AI
chance = false;
return chance;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiAttackController;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -10,14 +12,13 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
public class BidLifeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -26,31 +27,31 @@ public class BidLifeAi extends SpellAbilityAi {
if (tgt.canTgtCreature()) {
List<Card> list = CardLists.getTargetableCards(AiAttackController.choosePreferredDefenderPlayer(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
Card c = ComputerUtilCard.getBestCreatureAI(list);
if (sa.canTarget(c)) {
sa.getTargets().add(c);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else if (tgt.getZone().contains(ZoneType.Stack)) {
if (game.getStack().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final SpellAbility topSA = game.getStack().peekAbility();
if (!topSA.isCounterableBy(sa) || aiPlayer.equals(topSA.getActivatingPlayer())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.canTargetSpellAbility(topSA)) {
sa.getTargets().add(topSA);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
}
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -17,6 +17,8 @@
*/
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -46,9 +48,9 @@ public final class BondAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
} // end bondCanPlayAI()
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
@@ -56,7 +58,7 @@ public final class BondAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
return true;
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialAiLogic;
import forge.ai.SpecialCardAi;
@@ -21,16 +23,18 @@ public class BranchAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("GrislySigil".equals(aiLogic)) {
return SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
boolean result = SpecialCardAi.GrislySigil.consider(aiPlayer, sa);
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
} else if ("BranchCounter".equals(aiLogic)) {
return SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa); // Bring the Ending, Anticognition (hacky implementation)
boolean result = SpecialAiLogic.doBranchCounterspellLogic(aiPlayer, sa);
return new AiAbilityDecision(result ? 100 : 0, result ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
} else if ("TgtAttacker".equals(aiLogic)) {
final Combat combat = aiPlayer.getGame().getCombat();
if (combat == null || combat.getAttackingPlayer() != aiPlayer) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final CardCollection attackers = combat.getAttackers();
@@ -45,16 +49,20 @@ public class BranchAi extends SpellAbilityAi {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(attackers));
}
return sa.isTargetNumberValid();
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
}
// TODO: expand for other cases where the AI is needed to make a decision on a branch
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return canPlayAI(aiPlayer, sa) || mandatory;
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (decision.willingToPlay() || mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -10,15 +12,15 @@ public class CannotPlayAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return false;
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return canPlayAI(aiPlayer, sa);
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.GameEntity;
import forge.game.player.Player;
@@ -15,34 +17,36 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
// TODO: Extend this if possible for cards that have this as an activated ability
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(aiPlayer, sa);
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
final String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("WeakestOppExceptCtrl")) {
PlayerCollection targetableOpps = aiPlayer.getOpponents();
targetableOpps.remove(sa.getHostCard().getController());
if (targetableOpps.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return true;
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
@@ -63,4 +67,3 @@ public class ChangeCombatantsAi extends SpellAbilityAi {
return (T)weakestTargetableOpp;
}
}

View File

@@ -1,9 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.card.Card;
@@ -21,7 +18,7 @@ public class ChangeTargetsAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Game game = sa.getHostCard().getGame();
final SpellAbility topSa = game.getStack().isEmpty() ? null
: ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
@@ -32,47 +29,50 @@ public class ChangeTargetsAi extends SpellAbilityAi {
// The AI can't otherwise play this ability, but should at least not
// miss mandatory activations (e.g. triggers).
return sa.isMandatory();
if (sa.isMandatory()) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
private boolean doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
private AiAbilityDecision doSpellMagnet(SpellAbility sa, SpellAbility topSa, Player aiPlayer) {
// For cards like Spellskite that retarget spells to itself
if (topSa == null) {
// nothing on stack, so nothing to target
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final TargetChoices topTargets = topSa.getTargets();
final Card topHost = topSa.getHostCard();
if (sa.getTargets().size() != 0 && sa.isTrigger()) {
if (!sa.getTargets().isEmpty() && sa.isTrigger()) {
// something was already chosen before (e.g. in response to a trigger - Mizzium Meddler), so just proceed
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (!topSa.usesTargeting() || topTargets.getTargetCards().contains(sa.getHostCard())) {
// if this does not target at all or already targets host, no need to redirect it again
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
for (Card tgt : topTargets.getTargetCards()) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals(tgt.getName()) && tgt.getController().equals(aiPlayer)) {
// We are already targeting at least one card with the same name (e.g. in presence of 2+ Spellskites),
// no need to retarget again to another one
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (topHost != null && !topHost.getController().isOpponentOf(aiPlayer)) {
// make sure not to redirect our own abilities
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (!topSa.canTarget(sa.getHostCard())) {
// don't try targeting it if we can't legally target the host card with it in the first place
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (!sa.canTarget(topSa)) {
// don't try retargeting a spell that the current card can't legally retarget (e.g. Muck Drubb + Lightning Bolt to the face)
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.getPayCosts().getCostMana() != null && sa.getPayCosts().getCostMana().getMana().hasPhyrexian()) {
@@ -85,22 +85,22 @@ public class ChangeTargetsAi extends SpellAbilityAi {
if (potentialDmg != -1 && potentialDmg <= payDamage && !canPay
&& topTargets.contains(aiPlayer)) {
// do not pay Phyrexian mana if the spell is a damaging one but it deals less damage or the same damage as we'll pay life
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
Card firstCard = topTargets.getFirstTargetedCard();
// if we're not the target don't intervene unless we can steal a buff
if (firstCard != null && !aiPlayer.equals(firstCard.getController()) && !topHost.getController().equals(firstCard.getController()) && !topHost.getController().getAllies().contains(firstCard.getController())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Player firstPlayer = topTargets.getFirstTargetedPlayer();
if (firstPlayer != null && !aiPlayer.equals(firstPlayer)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.resetTargets();
sa.getTargets().add(topSa);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -46,6 +46,30 @@ public class ChangeZoneAi extends SpellAbilityAi {
private static CardCollection multipleCardsToChoose = new CardCollection();
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
if (sa.isHidden()) {
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)
&& !"Battlefield".equals(sa.getParam("Destination")) && !source.isLand()) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source, sa)) {
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostDiscard) {
CostDiscard cd = (CostDiscard) part;
// this is mainly for typecycling
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
return false;
}
}
}
}
return true;
}
if (sa.isCraft()) {
CardCollection payingCards = new CardCollection();
int needed = 0;
@@ -129,14 +153,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
@Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
// Checks for "return true" unlike checkAiLogic()
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
multipleCardsToChoose.clear();
String aiLogic = sa.getParam("AILogic");
if (aiLogic != null) {
if (aiLogic.equals("Always")) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
@@ -156,10 +178,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (aiLogic.equals("MazesEnd")) {
return SpecialCardAi.MazesEnd.consider(aiPlayer, sa);
} else if (aiLogic.equals("Pongify")) {
return sa.isTargetNumberValid(); // Pre-targeted in checkAiLogic
if (sa.isTargetNumberValid()) {
// Pre-targeted in checkAiLogic
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (aiLogic.equals("ReturnCastable")) {
return !sa.getHostCard().getExiledCards().isEmpty()
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false);
if (!sa.getHostCard().getExiledCards().isEmpty()
&& ComputerUtilMana.canPayManaCost(sa.getHostCard().getExiledCards().getFirst().getFirstSpellAbility(), aiPlayer, 0, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (sa.isHidden()) {
@@ -178,7 +208,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
if (sa.isHidden()) {
return hiddenOriginPlayDrawbackAI(aiPlayer, sa);
}
@@ -197,7 +227,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (sa.isReplacementAbility() && "Command".equals(sa.getParam("Destination")) && "ReplacedCard".equals(sa.getParam("Defined"))) {
@@ -206,10 +236,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if ("Always".equals(aiLogic)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if ("IfNotBuffed".equals(aiLogic)) {
if (ComputerUtilCard.isUselessCreature(aiPlayer, sa.getHostCard())) {
return true; // debuffed by opponent's auras to the level that it becomes useless
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
int delta = 0;
for (Card enc : sa.getHostCard().getEnchantedBy()) {
@@ -219,9 +249,17 @@ public class ChangeZoneAi extends SpellAbilityAi {
delta++;
}
}
return delta <= 0;
if (delta <= 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("SaviorOfOllenbock".equals(aiLogic)) {
return SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa);
if (SpecialCardAi.SaviorOfOllenbock.consider(aiPlayer, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (sa.isHidden()) {
@@ -250,10 +288,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static boolean hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
final Cost abCost = sa.getPayCosts();
private static AiAbilityDecision hiddenOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Fetching should occur fairly often as it helps cast more spells, and have access to more mana
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final String aiLogic = sa.getParamOrDefault("AILogic", "");
@@ -267,44 +303,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
} catch (IllegalArgumentException ex) {
// This happens when Origin is something like
// "Graveyard,Library" (Doomsday)
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
final String destination = sa.getParam("Destination");
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
&& !(destination.equals("Battlefield") && !source.isLand())) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
for (final CostPart part : abCost.getCostParts()) {
if (part instanceof CostDiscard) {
CostDiscard cd = (CostDiscard) part;
// this is mainly for typecycling
if (!cd.payCostFromSource() || !ComputerUtil.isWorseThanDraw(ai, source)) {
return false;
}
}
}
}
if (sa.isNinjutsu()) {
if (!source.ignoreLegendRule() && ai.isCardInPlay(source.getName())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WouldDestroyLegend);
}
if (ai.getGame().getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
if (ai.getGame().getCombat() == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
List<Card> attackers = ai.getGame().getCombat().getUnblockedAttackers();
boolean lowerCMC = false;
@@ -315,26 +328,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (!lowerCMC) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
// don't play if the conditions aren't met, unless it would trigger a beneficial sub-condition
if (!activateForCost && !sa.metConditions()) {
final AbilitySub abSub = sa.getSubAbility();
if (abSub != null && !sa.isWrapper() && "True".equals(source.getSVar("AIPlayForSub"))) {
if (!abSub.metConditions()) {
return false;
}
} else {
return false;
}
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
Iterable<Player> pDefined = Lists.newArrayList(source.getController());
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -347,7 +343,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.getTargets().add(ai);
}
if (!sa.isTargetNumberValid()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
pDefined = sa.getTargets().getTargetPlayers();
} else {
@@ -391,12 +387,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (!activateForCost && list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if ("Atarka's Command".equals(sourceName)
&& (list.size() < 2 || ai.getLandsPlayedThisTurn() < 1)) {
// be strict on playing lands off charms
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
String num = sa.getParamOrDefault("ChangeNum", "1");
@@ -404,55 +400,60 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (sa.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value.
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (xPay == 0) return false;
if (xPay == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
xPay = Math.min(xPay, list.size());
sa.setXManaCostPaid(xPay);
} else {
// Figure out the X amount, bail if it's zero (nothing will change zone).
int xValue = AbilityUtils.calculateAmount(source, "X", sa);
if (xValue == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
}
}
if (sourceName.equals("Temur Sabertooth")) {
// activated bounce + pump
if (ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible")) ||
ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility())) {
boolean pumpDecision = ComputerUtilCard.shouldPumpCard(ai, sa.getSubAbility(), source, 0, 0, Arrays.asList("Indestructible"));
AiAbilityDecision saveDecision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa.getSubAbility());
if (pumpDecision || saveDecision.willingToPlay()) {
for (Card c : list) {
if (ComputerUtilCard.evaluateCreature(c) < ComputerUtilCard.evaluateCreature(source)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.ResponseToStackResolve);
}
}
}
return canBouncePermanent(ai, sa, list) != null;
if (canBouncePermanent(ai, sa, list) != null) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (ComputerUtil.playImmediately(ai, sa)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// don't use fetching to top of library/graveyard before main2
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& !sa.hasParam("ActivationPhases")) {
if (!destination.equals("Battlefield") && !destination.equals("Hand")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Only tutor something in main1 if hand is almost empty
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && destination.equals("Hand")
&& !aiLogic.equals("AnyMainPhase")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (ComputerUtil.waitForBlocking(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -464,7 +465,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static boolean hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
private static AiAbilityDecision hiddenOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
// if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -476,11 +477,11 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (!isCurse && sa.canTarget(aiPlayer)) {
sa.getTargets().add(aiPlayer);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -494,23 +495,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a boolean.
* @return a boolean.
*/
private static boolean hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
private static AiAbilityDecision hiddenTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
// Fetching should occur fairly often as it helps cast more spells, and
// have access to more mana
if (sa.hasParam("AILogic")) {
if (sa.getParam("AILogic").equals("Never")) {
/*
* Hack to stop AI from using Aviary Mechanic's "may bounce" trigger.
* Ideally it should look for a good bounce target like "Pacifism"-victims
* but there is no simple way to check that. It is preferable for the AI
* to make sub-optimal choices (waste bounce) than to make obvious mistakes
* (bounce useful permanent).
*/
return false;
}
}
List<ZoneType> origin = new ArrayList<>();
if (sa.hasParam("Origin")) {
origin = ZoneType.listValueOf(sa.getParam("Origin"));
@@ -545,15 +533,15 @@ public class ChangeZoneAi extends SpellAbilityAi {
pDefined = sa.getTargets().getTargetPlayers();
if (Iterables.isEmpty(pDefined)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
pDefined = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
}
@@ -567,10 +555,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// *********** Utility functions for Hidden ********************
@@ -673,7 +661,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static boolean knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
private static AiAbilityDecision knownOriginCanPlayAI(final Player ai, final SpellAbility sa) {
// Retrieve either this card, or target Cards in Graveyard
final List<ZoneType> origin = Lists.newArrayList();
@@ -685,20 +673,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if (sa.usesTargeting()) {
if (!isPreferredTarget(ai, sa, false, false)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
// non-targeted retrieval
final List<Card> retrieval = sa.knownDetermineDefined(sa.getParam("Defined"));
if (retrieval == null || retrieval.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// return this card from graveyard: cards like Hammer of Bogardan
@@ -709,7 +693,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// (dying or losing control of)
if (origin.contains(ZoneType.Battlefield)) {
if (ai.getGame().getStack().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final AbilitySub abSub = sa.getSubAbility();
@@ -722,7 +706,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!(destination.equals(ZoneType.Exile)
&& (subApi == ApiType.DelayedTrigger || subApi == ApiType.ChangeZone || "DelayedBlink".equals(sa.getParam("AILogic"))))
&& !destination.equals(ZoneType.Hand)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final List<GameObject> objects = ComputerUtil.predictThreatenedObjects(ai, sa);
@@ -734,13 +718,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (!contains) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (destination == ZoneType.Battlefield) {
if (ComputerUtil.isETBprevented(retrieval.get(0))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// predict whether something may put a ETBing creature below zero toughness
@@ -750,7 +734,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card copy = CardCopyService.getLKICopy(c);
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
if (copy.getNetToughness() <= 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -764,13 +748,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (nothingWillReturn) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
final AbilitySub subAb = sa.getSubAbility();
return subAb == null || SpellApiToAi.Converter.get(subAb).chkDrawbackWithSubs(ai, subAb);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/*
@@ -784,7 +767,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("SurvivalOfTheFittest") || aiLogic.equals("AtOppEOT")) {
if (aiLogic.equals("SurvivalOfTheFittest")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
} else if (aiLogic.equals("Main1") && ph.is(PhaseType.MAIN1, ai)) {
return true;
@@ -843,16 +826,26 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean.
*/
private static boolean knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
private static AiAbilityDecision knownOriginPlayDrawbackAI(final Player aiPlayer, final SpellAbility sa) {
if ("MimicVat".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.MimicVat.considerExile(aiPlayer, sa);
if (SpecialCardAi.MimicVat.considerExile(aiPlayer, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (!sa.usesTargeting()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return isPreferredTarget(aiPlayer, sa, false, true);
if (!isPreferredTarget(aiPlayer, sa, false, true)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else {
// if we are here, we have a target
// so we can play the ability
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
/**
@@ -914,6 +907,9 @@ 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
}
// list = CardLists.canSubsequentlyTarget(list, sa);
if (sa.hasParam("AttachedTo")) {
list = CardLists.filter(list, c -> {
for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
@@ -1282,8 +1278,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
list.remove(choice);
if (sa.canTarget(choice)) {
sa.getTargets().add(choice);
}
}
// Honor the Single Zone restriction. For now, simply remove targets that do not belong to the same zone as the first targeted card.
// TODO: ideally the AI should consider at this point which targets exactly to pick (e.g. one card in the first player's graveyard
@@ -1448,6 +1446,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
// AI Targeting
Card choice = null;
// Filter out cards TargetsForEachPlayer
list = CardLists.canSubsequentlyTarget(list, sa);
if (!list.isEmpty()) {
Card mostExpensivePermanent = ComputerUtilCard.getMostExpensivePermanentAI(list);
if (mostExpensivePermanent.isCreature()
@@ -1485,7 +1486,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
}
if (choice == null) { // can't find anything left
if (sa.getTargets().size() == 0 || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
if (sa.getTargets().isEmpty() || sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
sa.resetTargets();
return false;
}
@@ -1513,13 +1514,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
* a boolean.
* @return a boolean.
*/
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
private static AiAbilityDecision knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
final String logic = sa.getParamOrDefault("AILogic", "");
if ("DeathgorgeScavenger".equals(logic)) {
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
if (SpecialCardAi.DeathgorgeScavenger.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("ExtraplanarLens".equals(logic)) {
return SpecialCardAi.ExtraplanarLens.consider(ai, sa);
if (SpecialCardAi.ExtraplanarLens.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("ExileCombatThreat".equals(logic)) {
return doExileCombatThreatLogic(ai, sa);
}
@@ -1531,14 +1540,27 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!list.isEmpty()) {
final Card attachedTo = list.get(0);
// This code is for the Dragon auras
return !attachedTo.getController().isOpponentOf(ai);
if (!attachedTo.getController().isOpponentOf(ai)) {
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI is the controller of the attachedTo card, then it is a valid target.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
} else if (isPreferredTarget(ai, sa, mandatory, true)) {
// do nothing
} else return isUnpreferredTarget(ai, sa, mandatory);
} else {
if (isUnpreferredTarget(ai, sa, mandatory)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI is not the controller of the attachedTo card, then it is not a valid target.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
public static Card chooseCardToHiddenOriginChangeZone(ZoneType destination, List<ZoneType> origin, SpellAbility sa, CardCollection fetchList, Player player, final Player decider) {
@@ -1572,7 +1594,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
} else if (logic.startsWith("ExilePreference")) {
return doExilePreferenceLogic(decider, sa, fetchList);
} else if (logic.equals("BounceOwnTrigger")) {
return doBounceOwnTriggerLogic(decider, fetchList);
return doBounceOwnTriggerLogic(decider, sa, fetchList);
}
}
if (fetchList.isEmpty()) {
@@ -1769,7 +1791,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
return super.chooseSingleAttackableEntity(ai, sa, options, params);
}
private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
private AiAbilityDecision doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
@@ -1788,14 +1810,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
sa.resetTargets();
sa.getTargets().add(bestRet);
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
private AiAbilityDecision doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
PhaseHandler ph = ai.getGame().getPhaseHandler();
String logic = sa.getParam("AILogic");
@@ -1803,7 +1825,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!ph.is(PhaseType.MAIN2)) {
// Should be given a chance to cast other spells as well as to use a previously upgraded creature
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
@@ -1842,15 +1864,14 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (!listGoal.isEmpty()) {
// make sure we're upgrading sacCMC->goalCMC
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + sacCMC);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
// no candidates to upgrade
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
public boolean doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision doReturnCommanderLogic(SpellAbility sa, Player aiPlayer) {
@SuppressWarnings("unchecked")
Map<AbilityKey, Object> originalParams = (Map<AbilityKey, Object>)sa.getReplacingObject(AbilityKey.OriginalParams);
SpellAbility causeSa = (SpellAbility)originalParams.get(AbilityKey.Cause);
@@ -1859,13 +1880,13 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (Objects.equals(ZoneType.Hand, destination)) {
// If the commander is being moved to your hand, don't replace since its easier to cast it again
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Squee, the Immortal: easier to recast it (the call below has to be "contains" since SA is an intrinsic effect)
if (sa.getHostCard().getName().contains("Squee, the Immortal") &&
(destination == ZoneType.Graveyard || destination == ZoneType.Exile)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (causeSa != null && (causeSub = causeSa.getSubAbility()) != null) {
@@ -1874,28 +1895,38 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (subApi == ApiType.ChangeZone && "Exile".equals(causeSub.getParam("Origin"))
&& "Battlefield".equals(causeSub.getParam("Destination"))) {
// A blink effect implemented using ChangeZone API
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else // This is an intrinsic effect that blinks the card (e.g. Obzedat, Ghost Council), no need to
// return the commander to the Command zone.
if (subApi == ApiType.DelayedTrigger) {
SpellAbility exec = causeSub.getAdditionalAbility("Execute");
if (exec != null && exec.getApi() == ApiType.ChangeZone) {
// A blink effect implemented using a delayed trigger
return !"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"));
if (!"Exile".equals(exec.getParam("Origin")) || !"Battlefield".equals(exec.getParam("Destination"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
if (causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|| !causeSa.getActivatingPlayer().equals(aiPlayer)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else return causeSa.getHostCard() == null || !causeSa.getHostCard().equals(sa.getReplacingObject(AbilityKey.Card))
|| !causeSa.getActivatingPlayer().equals(aiPlayer);
}
// Normally we want the commander back in Command zone to recast him later
return true;
// Normally we want the commander back in Command zone to recast it later
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
public static boolean doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
public static AiAbilityDecision doExileCombatThreatLogic(final Player aiPlayer, final SpellAbility sa) {
final Combat combat = aiPlayer.getGame().getCombat();
if (combat == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
Card choice = null;
@@ -1930,9 +1961,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (choice != null) {
sa.getTargets().add(choice);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
public static Card doExilePreferenceLogic(final Player aiPlayer, final SpellAbility sa, CardCollection fetchList) {
@@ -2127,16 +2158,18 @@ public class ChangeZoneAi extends SpellAbilityAi {
return AiCardMemory.isRememberedCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
}
private static Card doBounceOwnTriggerLogic(Player ai, CardCollection choices) {
private static Card doBounceOwnTriggerLogic(Player ai, SpellAbility sa, CardCollection choices) {
CardCollection unprefChoices = CardLists.filter(choices, c -> !c.isToken() && c.getOwner().equals(ai));
// TODO check for threatened cards
CardCollection prefChoices = CardLists.filter(unprefChoices, c -> c.hasETBTrigger(false));
if (!prefChoices.isEmpty()) {
return ComputerUtilCard.getBestAI(prefChoices);
} else if (!unprefChoices.isEmpty()) {
return ComputerUtilCard.getWorstAI(unprefChoices);
} else {
return null;
}
if (!unprefChoices.isEmpty() && sa.getSubAbility() != null) {
// some extra benefit like First Responder
return ComputerUtilCard.getWorstAI(unprefChoices);
}
return null;
}
@Override

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
@@ -19,7 +21,7 @@ import java.util.Map;
public class ChangeZoneAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
// Change Zone All, can be any type moving from one zone to another
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
@@ -32,14 +34,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
boolean aiLogicAllowsDiscard = aiLogic.startsWith("DiscardAll");
if (!aiLogicAllowsDiscard) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CostNotAcceptable);
}
}
}
@@ -59,31 +61,29 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// Ugin AI: always try to sweep before considering +1
if (sourceName.equals("Ugin, the Spirit Dragon")) {
return SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
boolean result = SpecialCardAi.UginTheSpiritDragon.considerPWAbilityPriority(ai, sa, origin, oppType, computerType);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
if ("LivingDeath".equals(aiLogic)) {
// Living Death AI
return SpecialCardAi.LivingDeath.consider(ai, sa);
} else if ("Timetwister".equals(aiLogic)) {
// Timetwister AI
return SpecialCardAi.Timetwister.consider(ai, sa);
} else if ("RetDiscardedThisTurn".equals(aiLogic)) {
// e.g. Shadow of the Grave
return ai.getDiscardedThisTurn().size() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
boolean result = !ai.getDiscardedThisTurn().isEmpty() && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("ExileGraveyards".equals(aiLogic)) {
for (Player opp : ai.getOpponents()) {
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.CREATURES);
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("ManifestCreatsFromGraveyard".equals(aiLogic)) {
PlayerCollection players = ai.getOpponents();
players.add(ai);
@@ -98,68 +98,48 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
bestTgt = player;
}
}
if (bestTgt != null) {
sa.resetTargets();
sa.getTargets().add(bestTgt);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// TODO improve restrictions on when the AI would want to use this
// spBounceAll has some AI we can compare to.
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
if (!sa.usesTargeting()) {
// TODO: improve logic for non-targeted SAs of this type (most are currently AI:RemoveDeck:All, e.g. Memory Jar)
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
// get the one with the most handsize
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
}
} else if (origin.equals(ZoneType.Battlefield)) {
// this statement is assuming the AI is trying to use this spell offensively
// if the AI is using it defensively, then something else needs to occur
// if only creatures are affected evaluate both lists and pass only
// if human creatures are more valuable
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
// set the target
if (oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
computerType = new CardCollection();
}
int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units)
int nonCreatureEvalThreshold = 3; // CMC difference
if (ai.getController().isAI()) {
@@ -181,103 +161,80 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
// Life is in serious danger, return all creatures from the battlefield to wherever
// so they don't deal lethal damage
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard
.evaluateCreatureList(oppType)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} // mass zone change for non-creatures: evaluate both lists by CMC and pass only if human
// permanents are more valuable
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
.evaluatePermanentList(oppType)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Don't cast during main1?
if (game.getPhaseHandler().is(PhaseType.MAIN1, ai) && !aiLogic.equals("Main1")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TimingRestrictions);
}
} else if (origin.equals(ZoneType.Graveyard)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = Collections.max(oppList, AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty()) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
} else if (destination.equals(ZoneType.Library) && "Card.YouOwn".equals(sa.getParam("ChangeType"))) {
return (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
boolean result = (ai.getCardsIn(ZoneType.Graveyard).size() > ai.getCardsIn(ZoneType.Library).size())
&& !ComputerUtil.isPlayingReanimator(ai);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (origin.equals(ZoneType.Exile)) {
if (aiLogic.startsWith("DiscardAllAndRetExiled")) {
int numExiledWithSrc = CardLists.filter(ai.getCardsIn(ZoneType.Exile), CardPredicates.isExiledWith(source)).size();
int curHandSize = ai.getCardsIn(ZoneType.Hand).size();
// minimum card advantage unless the hand will be fully reloaded
int minAdv = aiLogic.contains(".minAdv") ? Integer.parseInt(aiLogic.substring(aiLogic.indexOf(".minAdv") + 7)) : 0;
boolean noDiscard = aiLogic.contains(".noDiscard");
if (numExiledWithSrc > curHandSize || (noDiscard && numExiledWithSrc > 0)) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
// Try to gain some card advantage if the card will die anyway
// TODO: ideally, should evaluate the hand value and not discard good hands to it
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
boolean result = (curHandSize + minAdv - 1 < numExiledWithSrc) || (!noDiscard && numExiledWithSrc >= ai.getMaxHandSize());
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (origin.equals(ZoneType.Stack)) {
// TODO
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (destination.equals(ZoneType.Battlefield)) {
if (sa.hasParam("GainControl")) {
// Check if the cards are valuable enough
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
if ((ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard
.evaluateCreatureList(oppType)) < 400) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
} else if ((ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
.evaluatePermanentList(oppType)) < 6) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// don't activate if human gets more back than AI does
if (CardLists.getNotType(oppType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(computerType) <= (ComputerUtilCard
.evaluateCreatureList(oppType) + 100)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
} else if (ComputerUtilCard.evaluatePermanentList(computerType) <= (ComputerUtilCard
.evaluatePermanentList(oppType) + 2)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
return (((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance);
boolean result = ((MyRandom.getRandom().nextFloat() < .8) || sa.isTrigger()) && chance;
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/**
@@ -292,11 +249,11 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
// if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something:
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/* (non-Javadoc)
@@ -328,127 +285,90 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, final SpellAbility sa, boolean mandatory) {
// Change Zone All, can be any type moving from one zone to another
protected AiAbilityDecision doTriggerNoCost(Player ai, final SpellAbility sa, boolean mandatory) {
final ZoneType destination = ZoneType.smartValueOf(sa.getParam("Destination"));
final ZoneType origin = ZoneType.listValueOf(sa.getParam("Origin")).get(0);
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Profaner of the Dead")) {
// TODO: this is a stub to prevent the AI from crashing the game when, for instance, playing the opponent's
// Profaner from exile without paying its mana cost. Otherwise the card is marked AI:RemoveDeck:All and
// there is no specific AI to support playing it in a smarter way. Feel free to expand.
return ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
boolean result = ai.getOpponents().getCardsIn(origin).anyMatch(CardPredicates.CREATURES);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
CardCollectionView humanType = ai.getOpponents().getCardsIn(origin);
humanType = AbilityUtils.filterListByType(humanType, sa.getParam("ChangeType"), sa);
CardCollectionView computerType = ai.getCardsIn(origin);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
// TODO improve restrictions on when the AI would want to use this
// spBounceAll has some AI we can compare to.
if (origin.equals(ZoneType.Hand) || origin.equals(ZoneType.Library)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
// get the one with the most handsize
Player oppTarget = oppList.max(PlayerPredicates.compareByZoneSize(origin));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Hand).isEmpty() || mandatory) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
}
} else if (origin.equals(ZoneType.Battlefield)) {
// if mandatory, no need to evaluate
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// this statement is assuming the AI is trying to use this spell offensively
// if the AI is using it defensively, then something else needs to occur
// if only creatures are affected evaluate both lists and pass only
// if human creatures are more valuable
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(computerType) >= ComputerUtilCard.evaluateCreatureList(humanType)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are more valuable
else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
return false;
} else if (ComputerUtilCard.evaluatePermanentList(computerType) >= ComputerUtilCard.evaluatePermanentList(humanType)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (origin.equals(ZoneType.Graveyard)) {
if (sa.usesTargeting()) {
// search targetable Opponents
final PlayerCollection oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
if (mandatory && !sa.isTargetNumberValid() && sa.canTarget(ai)) {
sa.resetTargets();
sa.getTargets().add(ai);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return sa.isTargetNumberValid();
return sa.isTargetNumberValid() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
// get the one with the most in graveyard
// zone is visible so evaluate which would be hurt the most
Player oppTarget = oppList.max(
AiPlayerPredicates.compareByZoneValue(sa.getParam("ChangeType"), origin, sa));
// set the target
if (!oppTarget.getCardsIn(ZoneType.Graveyard).isEmpty() || mandatory) {
sa.resetTargets();
sa.getTargets().add(oppTarget);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
}
} else if (origin.equals(ZoneType.Exile)) {
} else if (origin.equals(ZoneType.Stack)) {
// currently only exists indirectly (e.g. Summary Dismissal via PlayAi)
}
if (destination.equals(ZoneType.Battlefield)) {
// if mandatory, no need to evaluate
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (sa.hasParam("GainControl")) {
// Check if the cards are valuable enough
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
return (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
return (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
boolean result = (ComputerUtilCard.evaluateCreatureList(computerType) + ComputerUtilCard.evaluateCreatureList(humanType)) >= 1;
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean result = (ComputerUtilCard.evaluatePermanentList(computerType) + ComputerUtilCard
.evaluatePermanentList(humanType)) >= 1;
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// don't activate if human gets more back than AI does
if (CardLists.getNotType(humanType, "Creature").isEmpty() && CardLists.getNotType(computerType, "Creature").isEmpty()) {
return ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
} // otherwise evaluate both lists by CMC and pass only if human
// permanents are less valuable
return ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
boolean result = ComputerUtilCard.evaluateCreatureList(computerType) > ComputerUtilCard.evaluateCreatureList(humanType);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
boolean result = ComputerUtilCard.evaluatePermanentList(computerType) > ComputerUtilCard.evaluatePermanentList(humanType);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -9,7 +9,6 @@ import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.collect.FCollection;
import java.util.Collections;
@@ -18,7 +17,7 @@ import java.util.Map;
public class CharmAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
List<AbilitySub> choices = CharmEffect.makePossibleOptions(sa);
@@ -70,10 +69,10 @@ public class CharmAi extends SpellAbilityAi {
// Set minimum choices for triggers where chooseMultipleOptionsAi() returns null
chosenList = chooseOptionsAi(sa, choices, ai, true, num, min);
if (chosenList.isEmpty() && min != 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -81,7 +80,7 @@ public class CharmAi extends SpellAbilityAi {
sa.setChosenList(chosenList);
if (choiceForOpp) {
return true;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.isSpell()) {
@@ -89,8 +88,7 @@ public class CharmAi extends SpellAbilityAi {
CharmEffect.chainAbilities(sa, chosenList);
}
// prevent run-away activations - first time will always return true
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return super.checkApiLogic(ai, sa);
}
private List<AbilitySub> chooseOptionsAi(SpellAbility sa, List<AbilitySub> choices, final Player ai, boolean isTrigger, int num, int min) {
@@ -276,10 +274,10 @@ public class CharmAi extends SpellAbilityAi {
}
@Override
public boolean chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
public AiAbilityDecision chkDrawbackWithSubs(Player aiPlayer, AbilitySub ab) {
// choices were already targeted
if (ab.getRootAbility().getChosenList() != null) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return super.chkDrawbackWithSubs(aiPlayer, ab);
}

View File

@@ -7,7 +7,6 @@ import forge.game.Game;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerPredicates;
@@ -26,19 +25,19 @@ public class ChooseCardAi extends SpellAbilityAi {
* The rest of the logic not covered by the canPlayAI template is defined here
*/
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.usesTargeting()) {
sa.resetTargets();
// search targetable Opponents
final List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(Iterables.getFirst(oppList, null));
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -135,23 +134,14 @@ public class ChooseCardAi extends SpellAbilityAi {
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
if (sa.hasParam("AILogic") && !checkAiLogic(ai, sa, sa.getParam("AILogic"))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return checkApiLogic(ai, sa);
}
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("AtOppEOT")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
}
return super.checkPhaseRestrictions(ai, sa, ph);
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)
*/

View File

@@ -22,16 +22,20 @@ import java.util.Map;
public class ChooseCardNameAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
if (sa.hasParam("AILogic")) {
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
String logic = sa.getParam("AILogic");
if (logic.equals("CursedScroll")) {
return SpecialCardAi.CursedScroll.consider(ai, sa);
if (SpecialCardAi.CursedScroll.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -43,13 +47,13 @@ public class ChooseCardNameAi extends SpellAbilityAi {
sa.getTargets().add(ai);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("PithingNeedle".equals(aiLogic)) {
// Make sure theres something in play worth Needlings.
@@ -57,18 +61,27 @@ public class ChooseCardNameAi extends SpellAbilityAi {
CardCollection oppPerms = CardLists.getValidCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), "Card.OppCtrl+hasNonManaActivatedAbility", ai, sa.getHostCard(), sa);
if (oppPerms.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
Card card = ComputerUtilCard.getBestPlaneswalkerAI(oppPerms);
if (card != null) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// 5 percent chance to cast per opposing card with a non mana ability
return MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size();
if (MyRandom.getRandom().nextFloat() <= .05 * oppPerms.size()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return mandatory;
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.card.spellability.SpellAbility, java.util.List, boolean)

View File

@@ -11,40 +11,45 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
public class ChooseColorAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final PhaseHandler ph = game.getPhaseHandler();
if (!sa.hasParam("AILogic")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
}
final String logic = sa.getParam("AILogic");
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if ("Nykthos, Shrine to Nyx".equals(sourceName)) {
return SpecialCardAi.NykthosShrineToNyx.consider(ai, sa);
if (SpecialCardAi.NykthosShrineToNyx.consider(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if ("Oona, Queen of the Fae".equals(sourceName)) {
if (ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
// Set PayX here to maximum value.
sa.setXManaCostPaid(ComputerUtilCost.getMaxXValue(sa, ai, false));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if ("Addle".equals(sourceName)) {
return !ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
// TODO Why is this not in the AI logic?
// Why are we specifying the weakest opponent?
if (!ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
}
if (logic.equals("MostExcessOpponentControls")) {
@@ -54,10 +59,10 @@ public class ChooseColorAi extends SpellAbilityAi {
int excess = ComputerUtilCard.evaluatePermanentList(opplist) - ComputerUtilCard.evaluatePermanentList(ailist);
if (excess > 4) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("MostProminentInComputerDeck")) {
if ("Astral Cornucopia".equals(sourceName)) {
// activate in Main 2 hoping that the extra mana surplus will make a difference
@@ -65,22 +70,28 @@ public class ChooseColorAi extends SpellAbilityAi {
CardCollectionView permanents = CardLists.filter(ai.getCardsIn(ZoneType.Hand),
CardPredicates.NONLAND_PERMANENTS);
return permanents.size() > 0 && ph.is(PhaseType.MAIN2, ai);
if (!permanents.isEmpty() && ph.is(PhaseType.MAIN2, ai)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
}
} else if (logic.equals("HighestDevotionToColor")) {
// currently only works more or less reliably in Main2 to cast own spells
if (!ph.is(PhaseType.MAIN2, ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
}
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
}
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.Direction;
import forge.game.Game;
@@ -18,11 +20,11 @@ public class ChooseDirectionAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
final String logic = sa.getParam("AILogic");
final Game game = sa.getActivatingPlayer().getGame();
if (logic == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
} else {
if ("Aminatou".equals(logic)) {
CardCollection all = CardLists.filter(game.getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
@@ -33,19 +35,24 @@ public class ChooseDirectionAi extends SpellAbilityAi {
CardCollection right = CardLists.filterControlledBy(all, game.getNextPlayerAfter(ai, Direction.Right));
int leftValue = Aggregates.sum(left, Card::getCMC);
int rightValue = Aggregates.sum(right, Card::getCMC);
return aiValue <= leftValue && aiValue <= rightValue;
if (aiValue <= leftValue && aiValue <= rightValue) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return true;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return canPlay(ai, sa);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
}
}

View File

@@ -1,17 +1,18 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiAttackController;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
public class ChooseEvenOddAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
if (!sa.hasParam("AILogic")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
}
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -19,16 +20,17 @@ public class ChooseEvenOddAi extends SpellAbilityAi {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
}
}

View File

@@ -7,7 +7,6 @@ import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardCollectionView;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
@@ -20,7 +19,6 @@ import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
public class ChooseGenericAi extends SpellAbilityAi {
@Override
@@ -29,13 +27,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
return true;
} else if ("Pump".equals(aiLogic) || "BestOption".equals(aiLogic)) {
for (AbilitySub sb : sa.getAdditionalAbilityList("Choices")) {
if (SpellApiToAi.Converter.get(sb).canPlayAIWithSubs(ai, sb)) {
if (SpellApiToAi.Converter.get(sb).canPlayWithSubs(ai, sb).willingToPlay()) {
return true;
}
}
} else if ("AtOppEOT".equals(aiLogic)) {
PhaseHandler ph = ai.getGame().getPhaseHandler();
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai;
} else if ("Always".equals(aiLogic)) {
return true;
}
@@ -43,35 +38,46 @@ public class ChooseGenericAi extends SpellAbilityAi {
}
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
return sa.hasParam("AILogic");
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
if (sa.hasParam("AILogic")) {
// This is equivilant to what was here before but feels bad
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return sa.isTrigger() ? doTriggerAINoCost(aiPlayer, sa, sa.isMandatory()) : checkApiLogic(aiPlayer, sa);
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
AiAbilityDecision decision;
if (sa.isTrigger()) {
decision = doTriggerNoCost(aiPlayer, sa, sa.isMandatory());
} else {
decision = checkApiLogic(aiPlayer, sa);
}
return decision;
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if ("CombustibleGearhulk".equals(sa.getParam("AILogic")) || "SoulEcho".equals(sa.getParam("AILogic"))) {
for (final Player p : aiPlayer.getOpponents()) {
if (p.canBeTargetedBy(sa)) {
sa.resetTargets();
sa.getTargets().add(p);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return true; // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
return new AiAbilityDecision(100, AiPlayDecision.WillPlay); // perhaps the opponent(s) had Sigarda, Heron's Grace or another effect giving hexproof in play, still play the creature as 6/6
}
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
}
@Override
@@ -161,10 +167,10 @@ public class ChooseGenericAi extends SpellAbilityAi {
}
}
// FatespinnerSkipDraw,FatespinnerSkipMain,FatespinnerSkipCombat
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "Draw")) {
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.DRAW)) {
return skipDraw;
}
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, "BeginCombat")) {
if (game.getReplacementHandler().wouldPhaseBeSkipped(player, PhaseType.COMBAT_BEGIN)) {
return skipCombat;
}
@@ -262,7 +268,7 @@ public class ChooseGenericAi extends SpellAbilityAi {
List<SpellAbility> filtered = Lists.newArrayList();
// filter first for the spells which can be done
for (SpellAbility sp : spells) {
if (SpellApiToAi.Converter.get(sp).canPlayAIWithSubs(player, sp)) {
if (SpellApiToAi.Converter.get(sp).canPlayWithSubs(player, sp).willingToPlay()) {
filtered.add(sp);
}
}

View File

@@ -1,21 +1,18 @@
package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
public class ChooseNumberAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
} else if (aiLogic.equals("SweepCreatures")) {
int maxChoiceLimit = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Max"), sa);
int ownCreatureCount = aiPlayer.getCreaturesInPlay().size();
@@ -30,17 +27,24 @@ public class ChooseNumberAi extends SpellAbilityAi {
}
if (refOpp == null) {
return false; // no opponent has any creatures
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
int evalAI = ComputerUtilCard.evaluateCreatureList(aiPlayer.getCreaturesInPlay());
int evalOpp = ComputerUtilCard.evaluateCreatureList(refOpp.getCreaturesInPlay());
if (aiPlayer.getLifeLostLastTurn() + aiPlayer.getLifeLostThisTurn() == 0 && evalAI > evalOpp) {
return false; // we're not pressured and our stuff seems better, don't do it yet
// we're not pressured and our stuff seems better, don't do it yet
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
return ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit);
if (ownCreatureCount > oppMaxCreatureCount + 2 || ownCreatureCount < Math.min(oppMaxCreatureCount, maxChoiceLimit)) {
// we have more creatures than the opponent, or we have less than the opponent but more than the max choice limit
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// we have less creatures than the opponent and less than the max choice limit
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (sa.usesTargeting()) {
@@ -49,16 +53,17 @@ public class ChooseNumberAi extends SpellAbilityAi {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return chance;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
}
}

View File

@@ -2,6 +2,8 @@ package forge.ai.ability;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
@@ -15,18 +17,18 @@ import java.util.Map;
public class ChoosePlayerAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
return true;
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return canPlay(ai, sa);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return canPlayAI(ai, sa);
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
return canPlay(ai, sa);
}
@Override

View File

@@ -1,10 +1,7 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
@@ -14,7 +11,6 @@ import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
@@ -32,21 +28,13 @@ public class ChooseSourceAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
// TODO: AI Support! Currently this is copied from AF ChooseCard.
// When implementing AI, I believe AI also needs to be made aware of the damage sources chosen
// to be prevented (e.g. so the AI doesn't attack with a creature that will not deal any damage
// to the player because a CoP was pre-activated on it - unless, of course, there's another
// possible reason to attack with that creature).
final Card host = sa.getHostCard();
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
if (abCost != null) {
if (!willPayCosts(ai, sa, abCost, source)) {
return false;
}
}
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -54,7 +42,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
if (sa.hasParam("AILogic")) {
@@ -63,11 +51,11 @@ public class ChooseSourceAi extends SpellAbilityAi {
if (!game.getStack().isEmpty()) {
final SpellAbility topStack = game.getStack().peekAbility();
if (sa.hasParam("Choices") && !topStack.matchesValid(topStack.getHostCard(), sa.getParam("Choices").split(","))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
final ApiType threatApi = topStack.getApi();
if (threatApi != ApiType.DealDamage && threatApi != ApiType.DamageAll) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
final Card threatSource = topStack.getHostCard();
@@ -79,13 +67,17 @@ public class ChooseSourceAi extends SpellAbilityAi {
}
if (!objects.contains(ai) || topStack.hasParam("NoPrevention")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
int dmg = AbilityUtils.calculateAmount(threatSource, topStack.getParam("NumDmg"), topStack);
return ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0;
if (ComputerUtilCombat.predictDamageTo(ai, dmg, threatSource, false) > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (game.getPhaseHandler().getPhase() != PhaseType.COMBAT_DECLARE_BLOCKERS) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
CardCollectionView choices = game.getCardsIn(ZoneType.Battlefield);
if (sa.hasParam("Choices")) {
@@ -98,11 +90,13 @@ public class ChooseSourceAi extends SpellAbilityAi {
}
return ComputerUtilCombat.damageIfUnblocked(c, ai, combat, true) > 0;
});
return !choices.isEmpty();
if (choices.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override

View File

@@ -21,23 +21,37 @@ import java.util.Set;
public class ChooseTypeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingLogic);
} else if ("MostProminentComputerControls".equals(aiLogic)) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Mirror Entity Avatar")) {
return doMirrorEntityLogic(aiPlayer, sa);
if (doMirrorEntityLogic(aiPlayer, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return !chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty();
} else if ("MostProminentComputerControlsOrOwns".equals(aiLogic)) {
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty();
} else if ("MostProminentOppControls".equals(aiLogic)) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty();
}
return doTriggerAINoCost(aiPlayer, sa, false);
if (!chooseType(sa, aiPlayer.getCardsIn(ZoneType.Battlefield)).isEmpty()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("MostProminentComputerControlsOrOwns".equals(aiLogic)) {
return !chooseType(sa, aiPlayer.getCardsIn(Arrays.asList(ZoneType.Hand, ZoneType.Battlefield))).isEmpty()
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("MostProminentOppControls".equals(aiLogic)) {
return !chooseType(sa, aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield)).isEmpty()
? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return doTriggerNoCost(aiPlayer, sa, false);
}
private boolean doMirrorEntityLogic(Player aiPlayer, SpellAbility sa) {
@@ -101,7 +115,7 @@ public class ChooseTypeAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean isCurse = sa.isCurse();
if (sa.usesTargeting()) {
@@ -133,16 +147,16 @@ public class ChooseTypeAi extends SpellAbilityAi {
}
if (!sa.isTargetNumberValid()) {
return false; // nothing to target?
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
for (final Player p : AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa)) {
if (p.isOpponentOf(ai) && !mandatory && !isCurse) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private String chooseType(SpellAbility sa, CardCollectionView cards) {

View File

@@ -2,6 +2,8 @@ package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -22,14 +24,15 @@ public class ClashAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
*/
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
boolean legalAction = true;
if (sa.usesTargeting()) {
legalAction = selectTarget(aiPlayer, sa);
}
return legalAction;
return legalAction ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
/*
@@ -39,14 +42,17 @@ public class ClashAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
boolean legalAction = true;
if (sa.usesTargeting()) {
legalAction = selectTarget(ai, sa);
if (!legalAction) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
return legalAction;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/*
@@ -104,7 +110,6 @@ public class ClashAi extends SpellAbilityAi {
}
}
return sa.getTargets().size() > 0;
return !sa.getTargets().isEmpty();
}
}

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.game.ability.AbilityUtils;
@@ -12,7 +14,8 @@ import forge.game.trigger.TriggerType;
public class ClassLevelUpAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
// TODO does leveling up affect combat? Otherwise wait for Main2
Card host = sa.getHostCard();
final int level = host.getClassLevel() + 1;
for (StaticAbility stAb : host.getStaticAbilities()) {
@@ -25,12 +28,12 @@ public class ClassLevelUpAi extends SpellAbilityAi {
continue;
}
SpellAbility effect = t.ensureAbility();
if (!SpellApiToAi.Converter.get(effect).doTriggerAI(aiPlayer, effect, false)) {
return false;
if (!SpellApiToAi.Converter.get(effect).doTrigger(aiPlayer, effect, false)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -14,7 +14,7 @@ public class CloakAi extends ManifestBaseAi {
// (e.g. Grafdigger's Cage)
Card topCopy = CardCopyService.getLKICopy(card);
topCopy.turnFaceDownNoUpdate();
topCopy.setCloaked(true);
topCopy.setCloaked(sa);
if (ComputerUtil.isETBprevented(topCopy)) {
return false;

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -18,7 +20,7 @@ import java.util.Map;
public class CloneAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final Game game = source.getGame();
@@ -36,10 +38,6 @@ public class CloneAi extends SpellAbilityAi {
// TODO - add some kind of check for during human turn to answer
// "Can I use this to block something?"
if (!checkPhaseRestrictions(ai, sa, game.getPhaseHandler())) {
return false;
}
PhaseHandler phase = game.getPhaseHandler();
if (!sa.usesTargeting()) {
@@ -66,18 +64,19 @@ public class CloneAi extends SpellAbilityAi {
}
if (!bFlag) { // All of the defined stuff is cloned, not very useful
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
} else {
sa.resetTargets();
useAbility &= cloneTgtAI(sa);
}
return useAbility;
} // end cloneCanPlayAI()
return useAbility ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
// AI should only activate this during Human's turn
boolean chance = true;
@@ -85,11 +84,12 @@ public class CloneAi extends SpellAbilityAi {
chance = cloneTgtAI(sa);
}
return chance;
return chance ? new AiAbilityDecision(100, AiPlayDecision.WillPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
Card host = sa.getHostCard();
boolean chance = true;
@@ -111,7 +111,11 @@ public class CloneAi extends SpellAbilityAi {
// Eventually, we can call the trigger of ETB abilities with
// not mandatory as part of the checks to cast something
return chance || mandatory;
if (mandatory || chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/**

View File

@@ -1,9 +1,7 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
@@ -13,9 +11,16 @@ import forge.game.zone.ZoneType;
public class ConniveAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
if (!ai.canDraw()) {
return false; // can't draw anything
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card host = sa.getHostCard();
final int num = AbilityUtils.calculateAmount(host, sa.getParamOrDefault("ConniveNum", "1"), sa);
if (num == 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
CardCollection list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
@@ -33,7 +38,7 @@ public class ConniveAi extends SpellAbilityAi {
sa.resetTargets();
while (sa.canAddMoreTarget()) {
if ((list.isEmpty() && sa.isTargetNumberValid() && !sa.getTargets().isEmpty())) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (list.isEmpty()) {
@@ -45,7 +50,7 @@ public class ConniveAi extends SpellAbilityAi {
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
return sa.isTargetNumberValid();
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
}
Card choice = ComputerUtilCard.getBestCreatureAI(list);
@@ -58,13 +63,17 @@ public class ConniveAi extends SpellAbilityAi {
list.clear();
}
}
return !sa.getTargets().isEmpty() && sa.isTargetNumberValid();
if (!sa.getTargets().isEmpty() && sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!ai.canDraw() && !mandatory) {
return false; // can't draw anything
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean preferred = true;
@@ -77,7 +86,7 @@ public class ConniveAi extends SpellAbilityAi {
while (sa.canAddMoreTarget()) {
if (mandatory) {
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (list.isEmpty() && preferred) {
@@ -90,14 +99,13 @@ public class ConniveAi extends SpellAbilityAi {
// Still an empty list, but we have to choose something (mandatory); expand targeting to
// include AI's own cards to see if there's anything targetable (e.g. Plague Belcher).
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
preferred = false;
}
}
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
return sa.isTargetNumberValid();
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
}
Card choice = ComputerUtilCard.getBestCreatureAI(list);
@@ -110,7 +118,10 @@ public class ConniveAi extends SpellAbilityAi {
list.clear();
}
}
return true;
return new AiAbilityDecision(
sa.isTargetNumberValid() && !sa.getTargets().isEmpty() ? 100 : 0,
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed
);
}
}

View File

@@ -1,9 +1,7 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtilCard;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
@@ -13,7 +11,6 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
public class ControlExchangeAi extends SpellAbilityAi {
@@ -21,7 +18,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
Card object1 = null;
Card object2 = null;
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -41,35 +38,40 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.getTargets().add(object2);
}
if (object1 == null || object2 == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (ComputerUtilCard.evaluateCreature(object1) > ComputerUtilCard.evaluateCreature(object2) + 40) {
sa.getTargets().add(object1);
return MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) {
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
if (mandatory) {
return chkAIDrawback(sa, aiPlayer) || sa.isTargetNumberValid();
AiAbilityDecision decision = chkDrawback(sa, aiPlayer);
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return decision;
} else {
return canPlayAI(aiPlayer, sa);
return canPlay(aiPlayer, sa);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
if (!sa.usesTargeting()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -90,7 +92,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty())
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
Card best = ComputerUtilCard.getBestAI(list);
@@ -106,7 +108,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
// Defined card is better than this one, try to avoid trade
if (!best.equals(realBest)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
@@ -115,10 +117,10 @@ public class ControlExchangeAi extends SpellAbilityAi {
return doTrigTwoTargetsLogic(aiPlayer, sa, best);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private boolean doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
private AiAbilityDecision doTrigTwoTargetsLogic(Player ai, SpellAbility sa, Card bestFirstTgt) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
final int creatureThreshold = 100; // TODO: make this configurable from the AI profile
final int nonCreatureThreshold = 2;
@@ -130,30 +132,30 @@ public class ControlExchangeAi extends SpellAbilityAi {
list = CardLists.getTargetableCards(list, sa);
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
Card aiWorst = ComputerUtilCard.getWorstAI(list);
if (aiWorst == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (aiWorst != bestFirstTgt) {
if (bestFirstTgt.isCreature() && aiWorst.isCreature()) {
if ((ComputerUtilCard.evaluateCreature(bestFirstTgt) > ComputerUtilCard.evaluateCreature(aiWorst) + creatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
// TODO: compare non-creatures by CMC - can be improved, at least shouldn't give control of things like the Power Nine
if ((bestFirstTgt.getCMC() > aiWorst.getCMC() + nonCreatureThreshold) || sa.isMandatory()) {
sa.getTargets().add(aiWorst);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
sa.clearTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}

View File

@@ -65,7 +65,7 @@ import java.util.Map;
*/
public class ControlGainAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
final List<String> lose = Lists.newArrayList();
if (sa.hasParam("LoseControl")) {
@@ -81,22 +81,30 @@ public class ControlGainAi extends SpellAbilityAi {
if (sa.hasParam("AllValid")) {
CardCollectionView tgtCards = opponents.getCardsIn(ZoneType.Battlefield);
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
return !tgtCards.isEmpty();
if (tgtCards.isEmpty()) {
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return true;
}
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
sa.resetTargets();
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
if (tgt.canOnlyTgtOpponent()) {
List<Player> oppList = opponents.filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (tgt.isRandomTarget()) {
@@ -111,12 +119,12 @@ public class ControlGainAi extends SpellAbilityAi {
if (lose.contains("EOT")
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& !sa.isTrigger()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.hasParam("Defined")) {
// no need to target, we'll pick up the target from Defined
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
CardCollection list = opponents.getCardsIn(ZoneType.Battlefield);
@@ -165,7 +173,7 @@ public class ControlGainAi extends SpellAbilityAi {
});
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
int creatures = 0, artifacts = 0, planeswalkers = 0, lands = 0, enchantments = 0;
@@ -194,7 +202,7 @@ public class ControlGainAi extends SpellAbilityAi {
if (list.isEmpty()) {
if ((sa.getTargets().size() < tgt.getMinTargets(sa.getHostCard(), sa)) || (sa.getTargets().size() == 0)) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -205,6 +213,9 @@ public class ControlGainAi extends SpellAbilityAi {
while (t == null) {
// filter by MustTarget requirement
CardCollection originalList = new CardCollection(list);
list = CardLists.canSubsequentlyTarget(list, sa);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
if (planeswalkers > 0) {
@@ -254,39 +265,41 @@ public class ControlGainAi extends SpellAbilityAi {
}
}
return true;
return new AiAbilityDecision(
sa.isTargetNumberValid() ? 100 : 0,
sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.TargetingFailed);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) {
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
if (sa.hasParam("TargetingPlayer") || (!this.canPlayAI(ai, sa) && mandatory)) {
if (sa.hasParam("TargetingPlayer") || (mandatory && !this.canPlay(ai, sa).willingToPlay())) {
if (sa.getTargetRestrictions().canOnlyTgtOpponent()) {
List<Player> oppList = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
if (oppList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
sa.getTargets().add(Aggregates.random(oppList));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
List<Card> list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, final Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, final Player ai) {
final Game game = ai.getGame();
// Special card logic that is processed elsewhere
@@ -302,7 +315,7 @@ public class ControlGainAi extends SpellAbilityAi {
CardCollectionView tgtCards = ai.getOpponents().getCardsIn(ZoneType.Battlefield);
tgtCards = AbilityUtils.filterListByType(tgtCards, sa.getParam("AllValid"), sa);
if (tgtCards.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
}
final List<String> lose = Lists.newArrayList();
@@ -311,10 +324,14 @@ public class ControlGainAi extends SpellAbilityAi {
lose.addAll(Lists.newArrayList(sa.getParam("LoseControl").split(",")));
}
return !lose.contains("EOT")
|| !game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS);
if (lose.contains("EOT")
&& game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
} else {
return this.canPlayAI(ai, sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
return this.canPlay(ai, sa);
}
}

View File

@@ -18,6 +18,8 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -41,24 +43,22 @@ import java.util.Map;
*/
public class ControlGainVariantAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
String logic = sa.getParam("AILogic");
if ("GainControlOwns".equals(logic)) {
List<Card> list = CardLists.filter(ai.getGame().getCardsIn(ZoneType.Battlefield), crd -> crd.isCreature() && !crd.getController().equals(crd.getOwner()));
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
for (final Card c : list) {
if (ai.equals(c.getController())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override

View File

@@ -22,45 +22,52 @@ import java.util.function.Predicate;
public class CopyPermanentAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
Card source = sa.getHostCard();
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if ("MomirAvatar".equals(aiLogic)) {
return SpecialCardAi.MomirVigAvatar.consider(aiPlayer, sa);
} else if ("MimicVat".equals(aiLogic)) {
return SpecialCardAi.MimicVat.considerCopy(aiPlayer, sa);
} else if ("AtEOT".equals(aiLogic)) {
return ph.is(PhaseType.END_OF_TURN);
} else if ("AtOppEOT".equals(aiLogic)) {
return ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn() != aiPlayer;
} else if ("DuplicatePerms".equals(aiLogic)) {
if (ph.is(PhaseType.END_OF_TURN)) {
if (ph.getPlayerTurn() == aiPlayer) {
// If it's the AI's turn, it can activate at EOT
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If it's not the AI's turn, it can't activate at EOT
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// Not at EOT phase
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
} if ("DuplicatePerms".equals(aiLogic)) {
final List<Card> valid = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if (valid.size() < 2) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
}
if (sa.hasParam("AtEOT") && !ph.is(PhaseType.MAIN1)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
if (sa.hasParam("Defined")) {
// If there needs to be an imprinted card, don't activate the ability if nothing was imprinted yet (e.g. Mimic Vat)
if (sa.getParam("Defined").equals("Imprinted.ExiledWithSource") && source.getImprintedCards().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
}
if (sa.isEmbalm() || sa.isEternalize()) {
// E.g. Vizier of Many Faces: check to make sure it makes sense to make the token now
if (ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa) != AiPlayDecision.WillPlay) {
return false;
AiPlayDecision decision = ComputerUtilCard.checkNeedsToPlayReqs(sa.getHostCard(), sa);
if (decision != AiPlayDecision.WillPlay) {
return new AiAbilityDecision(0, decision);
}
}
@@ -75,37 +82,45 @@ public class CopyPermanentAi extends SpellAbilityAi {
sa.resetTargets();
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer()) {
if (!sa.isCurse()) {
if (sa.canTarget(aiPlayer)) {
sa.getTargets().add(aiPlayer);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
for (Player p : aiPlayer.getYourTeam()) {
if (sa.canTarget(p)) {
sa.getTargets().add(p);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
for (Player p : aiPlayer.getOpponents()) {
if (sa.canTarget(p)) {
sa.getTargets().add(p);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
return doTriggerAINoCost(aiPlayer, sa, false);
return doTriggerNoCost(aiPlayer, sa, false);
}
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, SpellAbility sa, boolean mandatory) {
final Card host = sa.getHostCard();
final Player activator = sa.getActivatingPlayer();
final Game game = host.getGame();
@@ -128,13 +143,13 @@ public class CopyPermanentAi extends SpellAbilityAi {
//Nothing to target
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
CardCollection betterList = CardLists.filter(list, CardPredicates.isRemAIDeck().negate());
if (betterList.isEmpty()) {
if (!mandatory) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
list = betterList;
@@ -146,16 +161,18 @@ public class CopyPermanentAi extends SpellAbilityAi {
if (felidarGuardian.size() > 0) {
// can copy a Felidar Guardian and combo off, so let's do it
sa.getTargets().add(felidarGuardian.get(0));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
// target loop
while (sa.canAddMoreTarget()) {
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -175,9 +192,9 @@ public class CopyPermanentAi extends SpellAbilityAi {
}
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -192,20 +209,22 @@ public class CopyPermanentAi extends SpellAbilityAi {
choices = CardLists.getValidCards(choices, sa.getParam("Choices"), activator, host, sa);
Collection<Card> betterChoices = getBetterOptions(aiPlayer, sa, choices, !mandatory);
if (betterChoices.isEmpty()) {
return mandatory;
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// if no targeting, it should always be ok
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
}
if ("TriggeredCardController".equals(sa.getParam("Controller"))) {
Card trigCard = (Card)sa.getTriggeringObject(AbilityKey.Card);
if (!mandatory && trigCard != null && trigCard.getController().isOpponentOf(aiPlayer)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/* (non-Javadoc)

View File

@@ -17,14 +17,15 @@ import java.util.Map;
public class CopySpellAbilityAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player aiPlayer, SpellAbility sa) {
Game game = aiPlayer.getGame();
int chance = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.CHANCE_TO_COPY_OWN_SPELL_WHILE_ON_STACK);
int diff = ((PlayerControllerAi)aiPlayer.getController()).getAi().getIntProperty(AiProps.ALWAYS_COPY_SPELL_IF_CMC_DIFF);
String logic = sa.getParamOrDefault("AILogic", "");
if (game.getStack().isEmpty()) {
return sa.isMandatory() || "Always".equals(logic);
boolean result = sa.isMandatory() || "Always".equals(logic);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final SpellAbility top = game.getStack().peekAbility();
@@ -41,47 +42,40 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (!MyRandom.percentTrue(chance)
&& !"AlwaysIfViable".equals(logic)
&& !"OnceIfViable".equals(logic)
&& !"Always".equals(logic)
&& !"AlwaysCopyActivatedAbilities".equals(logic)) {
return false;
}
if ("OnceIfViable".equals(logic)) {
if (AiCardMemory.isRememberedCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
return false;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.usesTargeting()) {
// Filter AI-specific targets if provided
if ("OnlyOwned".equals(sa.getParam("AITgts"))) {
if (!top.getActivatingPlayer().equals(aiPlayer)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (top.isWrapper() || top.isActivatedAbility()) {
// Shouldn't even try with triggered or wrapped abilities at this time, will crash
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (top.getApi() == ApiType.CopySpellAbility) {
// Don't try to copy a copy ability, too complex for the AI to handle
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (top.getApi() == ApiType.Mana) {
// would lead to Stack Overflow by trying to play this again
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (top.getApi() == ApiType.DestroyAll || top.getApi() == ApiType.SacrificeAll || top.getApi() == ApiType.ChangeZoneAll || top.getApi() == ApiType.TapAll || top.getApi() == ApiType.UnattachAll) {
if (!top.usesTargeting() || top.getActivatingPlayer().equals(aiPlayer)) {
// If we activated a mass removal / mass tap / mass bounce / etc. spell, or if the opponent activated it but
// it can't be retargeted, no reason to copy this spell since it'll probably do the same thing and is useless as a copy
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (top.hasParam("ConditionManaSpent") || top.getHostCard().hasSVar("AINoCopy")) {
// Mana spent is not copied, so these spells generally do nothing when copied.
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (ComputerUtilCard.isCardRemAIDeck(top.getHostCard())) {
// Don't try to copy anything you can't understand how to handle
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// A copy is necessary to properly test the SA before targeting the copied spell, otherwise the copy SA will fizzle.
@@ -99,32 +93,49 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
}
if (decision == AiPlayDecision.WillPlay) {
sa.getTargets().add(top);
AiCardMemory.rememberCard(aiPlayer, sa.getHostCard(), AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, decision);
}
}
// the AI should not miss mandatory activations
return sa.isMandatory() || "Always".equals(logic);
boolean result = sa.isMandatory() || "Always".equals(logic);
return result ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
// the AI should not miss mandatory activations (e.g. Precursor Golem trigger)
String logic = sa.getParamOrDefault("AILogic", "");
return mandatory || logic.contains("Always"); // this includes logic like AlwaysIfViable
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (logic.contains("Always")) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
public AiAbilityDecision chkDrawback(final SpellAbility sa, final Player aiPlayer) {
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
}
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
return canPlayAI(aiPlayer, sa) || (sa.isMandatory() && super.chkAIDrawback(sa, aiPlayer));
AiAbilityDecision decision = canPlay(aiPlayer, sa);
if (!decision.willingToPlay()) {
if (sa.isMandatory()) {
return super.chkDrawback(sa, aiPlayer);
}
}
return decision;
}
@Override
@@ -138,7 +149,7 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
// Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then
// run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents.
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(player, sa);
return SpecialCardAi.ChainOfAcid.consider(player, sa).willingToPlay();
}
return true;

View File

@@ -26,13 +26,11 @@ import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
public class CounterAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
boolean toReturn = true;
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Game game = ai.getGame();
@@ -40,22 +38,12 @@ public class CounterAi extends SpellAbilityAi {
SpellAbility tgtSA = null;
if (game.getStack().isEmpty()) {
return false;
}
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if ("Force of Will".equals(sourceName)) {
if (!SpecialCardAi.ForceOfWill.consider(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -63,19 +51,19 @@ public class CounterAi extends SpellAbilityAi {
final SpellAbility topSA = ComputerUtilAbility.getTopSpellAbilityOnStack(game, sa);
if ((topSA.isSpell() && !topSA.isCounterableBy(sa)) || ai.getYourTeam().contains(topSA.getActivatingPlayer())) {
// might as well check for player's friendliness
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (sa.hasParam("ConditionWouldDestroy") && !CounterEffect.checkForConditionWouldDestroy(sa, topSA)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
// check if the top ability on the stack corresponds to the AI-specific targeting declaration, if provided
if (sa.hasParam("AITgts") && (topSA.getHostCard() == null
|| !topSA.getHostCard().isValid(sa.getParam("AITgts"), sa.getActivatingPlayer(), source, sa))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (sa.hasParam("CounterNoManaSpell") && topSA.getTotalManaSpent() > 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (sa.hasParam("UnlessCost") && "TargetedController".equals(sa.getParamOrDefault("UnlessPayer", "TargetedController"))) {
@@ -84,7 +72,7 @@ public class CounterAi extends SpellAbilityAi {
CostDiscard discardCost = unlessCost.getCostPartByType(CostDiscard.class);
if ("Hand".equals(discardCost.getType())) {
if (topSA.getActivatingPlayer().getCardsIn(ZoneType.Hand).size() < 2) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -100,10 +88,11 @@ public class CounterAi extends SpellAbilityAi {
tgtCMC += topSA.getPayCosts().getTotalMana().countX() > 0 ? 3 : 0; // TODO: somehow determine the value of X paid and account for it?
}
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
return false;
// This spell doesn't target. Must be a "Coutner All" or "Counter trigger" type of ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
@@ -122,13 +111,13 @@ public class CounterAi extends SpellAbilityAi {
}
if (toPay == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most of the time
if (!playReusable(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
}
@@ -147,15 +136,15 @@ public class CounterAi extends SpellAbilityAi {
if (sa.hasParam("AILogic")) {
String logic = sa.getParam("AILogic");
if ("Never".equals(logic)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.startsWith("MinCMC.")) { // TODO fix Daze and fold into AITgts
int minCMC = Integer.parseInt(logic.substring(7));
if (tgtCMC < minCMC) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("NullBrooch".equals(logic)) {
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -234,40 +223,40 @@ public class CounterAi extends SpellAbilityAi {
}
if (dontCounter) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return toReturn;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
return doTriggerAINoCost(aiPlayer, sa, true);
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
return doTriggerNoCost(aiPlayer, sa, true);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Game game = ai.getGame();
if (sa.usesTargeting()) {
if (game.getStack().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
sa.resetTargets();
if (mandatory && !sa.canAddMoreTarget()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
Pair<SpellAbility, Boolean> pair = chooseTargetSpellAbility(game, sa, ai, mandatory);
SpellAbility tgtSA = pair.getLeft();
if (tgtSA == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
sa.getTargets().add(tgtSA);
if (!mandatory && !pair.getRight()) {
// If not mandatory and not preferred, bail out after setting target
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
@@ -288,14 +277,13 @@ public class CounterAi extends SpellAbilityAi {
if (!mandatory) {
if (toPay == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
if (toPay <= usableManaSources) {
// If this is a reusable Resource, feel free to play it most
// of the time
// If this is a reusable Resource, feel free to play it most of the time
if (!playReusable(ai,sa) || (MyRandom.getRandom().nextFloat() < .4)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
}
}
@@ -312,7 +300,7 @@ public class CounterAi extends SpellAbilityAi {
// force the Human into making decisions)
// But really it should be more picky about how it counters things
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
public Pair<SpellAbility, Boolean> chooseTargetSpellAbility(Game game, SpellAbility sa, Player ai, boolean mandatory) {
@@ -362,11 +350,11 @@ public class CounterAi extends SpellAbilityAi {
@Override
public boolean willPayUnlessCost(SpellAbility sa, Player payer, Cost cost, boolean alreadyPaid, FCollectionView<Player> payers) {
// ward or human misplay
final Card source = sa.getHostCard();
final Game game = source.getGame();
List<SpellAbility> spells = AbilityUtils.getDefinedSpellAbilities(source, sa.getParamOrDefault("Defined", "Targeted"), sa);
for (SpellAbility toBeCountered : spells) {
// ward or human misplay
if (!toBeCountered.isCounterableBy(sa)) {
return false;
}
@@ -381,7 +369,7 @@ public class CounterAi extends SpellAbilityAi {
}
// no reason to pay if we don't plan to confirm
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false)) {
if (toBeCountered.isOptionalTrigger() && !SpellApiToAi.Converter.get(toBeCountered).doTriggerNoCostWithSubs(payer, toBeCountered, false).willingToPlay()) {
return false;
}
// TODO check hasFizzled

View File

@@ -45,13 +45,13 @@ public abstract class CountersAi extends SpellAbilityAi {
* </p>
*
* @param list
* a {@link forge.CardList} object.
* a {@link CardCollectionView} object.
* @param type
* a {@link java.lang.String} object.
* a {@link String} object.
* @param amount
* a int.
* @param newParam TODO
* @return a {@link forge.game.card.Card} object.
* @param ai a {@link Player} object.
* @return a {@link Card} object.
*/
public static Card chooseCursedTarget(final CardCollectionView list, final String type, final int amount, final Player ai) {
Card choice;
@@ -65,7 +65,7 @@ public abstract class CountersAi extends SpellAbilityAi {
// try to kill the best killable creature, or reduce the best one
// but try not to target a Undying Creature
final List<Card> killable = CardLists.getNotKeyword(CardLists.filterToughness(list, amount), Keyword.UNDYING);
if (killable.size() > 0) {
if (!killable.isEmpty()) {
choice = ComputerUtilCard.getBestCreatureAI(killable);
} else {
choice = ComputerUtilCard.getBestCreatureAI(list);
@@ -83,10 +83,10 @@ public abstract class CountersAi extends SpellAbilityAi {
* </p>
*
* @param list
* a {@link forge.CardList} object.
* a {@link CardCollectionView} object.
* @param type
* a {@link java.lang.String} object.
* @return a {@link forge.game.card.Card} object.
* a {@link String} object.
* @return a {@link Card} object.
*/
public static Card chooseBoonTarget(final CardCollectionView list, final String type) {
Card choice = null;
@@ -102,7 +102,7 @@ public abstract class CountersAi extends SpellAbilityAi {
} else if (type.equals("DIVINITY")) {
final CardCollection boon = CardLists.filter(list, c -> c.getCounters(CounterEnumType.DIVINITY) == 0);
choice = ComputerUtilCard.getMostExpensivePermanentAI(boon);
} else if (CounterType.get(type).isKeywordCounter()) {
} else if (CounterType.getType(type).isKeywordCounter()) {
choice = ComputerUtilCard.getBestCreatureAI(CardLists.getNotKeyword(list, type));
} else {
// The AI really should put counters on cards that can use it.

View File

@@ -1,9 +1,7 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
@@ -21,19 +19,25 @@ import java.util.Map;
public class CountersMoveAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
AiAbilityDecision decision = new AiAbilityDecision(100, AiPlayDecision.WillPlay);
if (sa.usesTargeting()) {
sa.resetTargets();
if (!moveTgtAI(ai, sa)) {
return false;
decision = moveTgtAI(ai, sa);
if (!decision.willingToPlay()) {
return decision;
}
}
if (!playReusable(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return MyRandom.getRandom().nextFloat() < .8f; // random success
if (MyRandom.getRandom().nextFloat() < .8f) {
return decision;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
@@ -109,12 +113,13 @@ public class CountersMoveAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(final Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(final Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!moveTgtAI(ai, sa) && !mandatory) {
return false;
AiAbilityDecision decision = moveTgtAI(ai, sa);
if (!decision.willingToPlay() && !mandatory) {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (!sa.isTargetNumberValid() && mandatory) {
@@ -122,18 +127,18 @@ public class CountersMoveAi extends SpellAbilityAi {
List<Card> tgtCards = CardLists.getTargetableCards(game.getCardsIn(ZoneType.Battlefield), sa);
if (tgtCards.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
final Card card = ComputerUtilCard.getWorstAI(tgtCards);
sa.getTargets().add(card);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// no target Probably something like Graft
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
final Card host = sa.getHostCard();
@@ -145,7 +150,7 @@ public class CountersMoveAi extends SpellAbilityAi {
final List<Card> destCards = AbilityUtils.getDefinedCards(host, sa.getParam("Defined"), sa);
if (srcCards.isEmpty() || destCards.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
final Card src = srcCards.get(0);
@@ -153,21 +158,21 @@ public class CountersMoveAi extends SpellAbilityAi {
// for such Trigger, do not move counter to another players creature
if (!dest.getController().equals(ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (ComputerUtilCard.isUselessCreature(ai, dest)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (dest.hasSVar("EndOfTurnLeavePlay")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (cType != null) {
if (!dest.canReceiveCounters(cType)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final int amount = calcAmount(sa, cType);
int a = src.getCounters(cType);
if (a < amount) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final Card srcCopy = CardCopyService.getLKICopy(src);
@@ -181,27 +186,31 @@ public class CountersMoveAi extends SpellAbilityAi {
int newEval = ComputerUtilCard.evaluateCreature(srcCopy) + ComputerUtilCard.evaluateCreature(destCopy);
if (newEval < oldEval) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
// check for some specific AI preferences
if ("DontMoveCounterIfLethal".equals(sa.getParam("AILogic"))) {
return !cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0;
if (!cType.is(CounterEnumType.P1P1) || src.getNetToughness() - src.getTempToughnessBoost() - 1 > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
// no target
return true;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
if (sa.usesTargeting()) {
sa.resetTargets();
return moveTgtAI(ai, sa);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private static int calcAmount(final SpellAbility sa, final CounterType cType) {
@@ -226,7 +235,7 @@ public class CountersMoveAi extends SpellAbilityAi {
return amount;
}
private boolean moveTgtAI(final Player ai, final SpellAbility sa) {
private AiAbilityDecision moveTgtAI(final Player ai, final SpellAbility sa) {
final Card host = sa.getHostCard();
final Game game = ai.getGame();
final String type = sa.getParam("CounterType");
@@ -244,7 +253,7 @@ public class CountersMoveAi extends SpellAbilityAi {
if (destCards.isEmpty()) {
// something went wrong
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
final Card dest = destCards.get(0);
@@ -253,7 +262,7 @@ public class CountersMoveAi extends SpellAbilityAi {
tgtCards.remove(dest);
if (cType != null && !dest.canReceiveCounters(cType)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// prefered logic for this: try to steal counter
@@ -285,7 +294,7 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -329,14 +338,14 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else if (sa.getMaxTargets() == 2) {
// TODO
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// SA uses target for Defined
// Source => Targeted
@@ -344,12 +353,12 @@ public class CountersMoveAi extends SpellAbilityAi {
if (srcCards.isEmpty()) {
// something went wrong
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
final Card src = srcCards.get(0);
if (cType != null && src.getCounters(cType) <= 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Card lkiWithCounters = CardCopyService.getLKICopy(src);
@@ -402,14 +411,14 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
final boolean isMandatoryTrigger = (sa.isTrigger() && !sa.isOptionalTrigger())
|| (sa.getRootAbility().isTrigger() && !sa.getRootAbility().isOptionalTrigger());
if (!isMandatoryTrigger) {
// no good target
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
@@ -439,10 +448,10 @@ public class CountersMoveAi extends SpellAbilityAi {
if (card != null) {
sa.getTargets().add(card);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}

View File

@@ -1,9 +1,7 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
@@ -21,10 +19,12 @@ import java.util.Map;
public class CountersMultiplyAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
final CounterType counterType = getCounterType(sa);
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
return setTargets(ai, sa);
}
if (!sa.usesTargeting()) {
final CounterType counterType = getCounterType(sa);
// defined are mostly Self or Creatures you control
CardCollection list = AbilityUtils.getDefinedCards(sa.getHostCard(), sa.getParam("Defined"), sa);
@@ -53,10 +53,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
});
if (list.isEmpty()) {
return false;
}
} else {
return setTargets(ai, sa);
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
return super.checkApiLogic(ai, sa);
@@ -85,24 +82,27 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!sa.usesTargeting()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (setTargets(ai, sa)) {
return true;
AiAbilityDecision decision = setTargets(ai, sa);
if (decision.willingToPlay()) {
return decision;
} else if (mandatory) {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
Card safeMatch = list.stream()
.filter(CardPredicates.hasCounters().negate())
.findFirst().orElse(null);
sa.getTargets().add(safeMatch == null ? list.getFirst() : safeMatch);
return true;
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
}
return mandatory;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
private CounterType getCounterType(SpellAbility sa) {
@@ -117,7 +117,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
return null;
}
private boolean setTargets(Player ai, SpellAbility sa) {
private AiAbilityDecision setTargets(Player ai, SpellAbility sa) {
final CounterType counterType = getCounterType(sa);
final Game game = ai.getGame();
@@ -154,7 +154,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
}
if (counterType == null || counterType.is(type)) {
addTargetsByCounterType(ai, sa, aiList, CounterType.get(type));
addTargetsByCounterType(ai, sa, aiList, type);
}
}
}
@@ -163,7 +163,7 @@ public class CountersMultiplyAi extends SpellAbilityAi {
if (!oppList.isEmpty()) {
// not enough targets
if (sa.canAddMoreTarget()) {
final CounterType type = CounterType.get(CounterEnumType.M1M1);
final CounterType type = CounterEnumType.M1M1;
if (counterType == null || counterType == type) {
addTargetsByCounterType(ai, sa, oppList, type);
}
@@ -173,10 +173,10 @@ public class CountersMultiplyAi extends SpellAbilityAi {
// targeting does failed
if (!sa.isTargetNumberValid() || sa.getTargets().size() == 0) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private void addTargetsByCounterType(final Player ai, final SpellAbility sa, final CardCollection list,

View File

@@ -16,7 +16,7 @@ import java.util.Map;
public class CountersProliferateAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final List<Card> cperms = Lists.newArrayList();
boolean allyExpOrEnergy = false;
@@ -68,25 +68,34 @@ public class CountersProliferateAi extends SpellAbilityAi {
}));
}
return !cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy;
if (!cperms.isEmpty() || !hperms.isEmpty() || opponentPoison || allyExpOrEnergy) {
// AI will play it if there are any counters to proliferate
// or if there are no counters, but AI has experience or energy counters
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
boolean chance = true;
// TODO Make sure Human has poison counters or there are some counters
// we want to proliferate
return chance;
return new AiAbilityDecision(
chance ? 100 : 0,
chance ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi
);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#chkAIDrawback(java.util.Map, forge.card.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return checkApiLogic(ai, sa);
@@ -101,7 +110,7 @@ public class CountersProliferateAi extends SpellAbilityAi {
public <T extends GameEntity> T chooseSingleEntity(Player ai, SpellAbility sa, Collection<T> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Proliferate is always optional for all, no need to select best
final CounterType poison = CounterType.get(CounterEnumType.POISON);
final CounterType poison = CounterEnumType.POISON;
boolean aggroAI = (((PlayerControllerAi) ai.getController()).getAi()).getBooleanProperty(AiProps.PLAY_AGGRO);
// because countertype can't be chosen anymore, only look for poison counters

View File

@@ -53,8 +53,7 @@ public class CountersPutAi extends CountersAi {
// disable moving counters (unless a specialized AI logic supports it)
for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostRemoveCounter) {
final CostRemoveCounter remCounter = (CostRemoveCounter) part;
if (part instanceof CostRemoveCounter remCounter) {
final CounterType counterType = remCounter.counter;
if (counterType.getName().equals(type) && !aiLogic.startsWith("MoveCounter")) {
return false;
@@ -98,7 +97,7 @@ public class CountersPutAi extends CountersAi {
}
}
if (sa.hasParam("LevelUp")) {
if (sa.isKeyword(Keyword.LEVEL_UP)) {
// creatures enchanted by curse auras have low priority
if (ph.getPhase().isBefore(PhaseType.MAIN2)) {
for (Card aura : source.getEnchantedBy()) {
@@ -119,7 +118,7 @@ public class CountersPutAi extends CountersAi {
}
@Override
protected boolean checkApiLogic(Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, final SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Cost abCost = sa.getPayCosts();
@@ -160,7 +159,7 @@ public class CountersPutAi extends CountersAi {
PlayerCollection poisonList = oppList.filter(PlayerPredicates.hasCounter(CounterEnumType.POISON, 9));
if (!poisonList.isEmpty()) {
sa.getTargets().add(poisonList.max(PlayerPredicates.compareByLife()));
return true;
return new AiAbilityDecision(1000, AiPlayDecision.WillPlay);
}
}
@@ -171,12 +170,12 @@ public class CountersPutAi extends CountersAi {
CardCollection oppCreatM1 = CardLists.filter(oppCreat, CardPredicates.hasCounter(CounterEnumType.M1M1));
oppCreatM1 = CardLists.getNotKeyword(oppCreatM1, Keyword.UNDYING);
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterType.get(CounterEnumType.M1M1)));
oppCreatM1 = CardLists.filter(oppCreatM1, input -> input.getNetToughness() <= 1 && input.canReceiveCounters(CounterEnumType.M1M1));
Card best = ComputerUtilCard.getBestAI(oppCreatM1);
if (best != null) {
sa.getTargets().add(best);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
CardCollection aiCreat = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
@@ -196,7 +195,7 @@ public class CountersPutAi extends CountersAi {
best = ComputerUtilCard.getBestAI(aiCreat);
if (best != null) {
sa.getTargets().add(best);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -205,28 +204,22 @@ public class CountersPutAi extends CountersAi {
if (!ai.getCounters().isEmpty()) {
if (!eachExisting || ai.getPoisonCounters() < 5) {
sa.getTargets().add(ai);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if ("Never".equals(logic)) {
return false;
} else if ("AlwaysWithNoTgt".equals(logic)) {
return true;
if ("AlwaysWithNoTgt".equals(logic)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if ("AristocratCounters".equals(logic)) {
return SpecialAiLogic.doAristocratWithCountersLogic(ai, sa);
} else if ("PayEnergy".equals(logic)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if ("PayEnergyConservatively".equals(logic)) {
boolean onlyInCombat = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
@@ -235,10 +228,10 @@ public class CountersPutAi extends CountersAi {
if (playAggro) {
// aggro profiles ignore conservative play for this AI logic
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (ph.inCombat() && source != null) {
if (ai.getGame().getCombat().isAttacking(source) && !onlyDefensive) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
} else if (ai.getGame().getCombat().isBlocking(source)) {
// when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker
CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(source);
@@ -248,28 +241,27 @@ public class CountersPutAi extends CountersAi {
int numActivations = ai.getCounters(CounterEnumType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
if (source.getNetToughness() + numActivations > totBlkPower
|| source.getNetPower() + numActivations >= totBlkToughness) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}
} else if (sa.getSubAbility() != null
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ANIMATED_THIS_TURN)) {
&& !source.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
// Bristling Hydra: save from death using a ping activation
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (ai.getCounters(CounterEnumType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
// outside of combat, this logic only works if the relevant AI profile option is enabled
// and if there is enough energy saved
if (!onlyInCombat) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
} else if (logic.equals("MarkOppCreature")) {
if (!ph.is(PhaseType.END_OF_TURN)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
Predicate<Card> predicate = CardPredicates.hasCounter(CounterType.getType(type));
@@ -281,12 +273,12 @@ public class CountersPutAi extends CountersAi {
Card bestCreat = ComputerUtilCard.getBestCreatureAI(oppCreats);
sa.resetTargets();
sa.getTargets().add(bestCreat);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (logic.equals("CheckDFC")) {
// for cards like Ludevic's Test Subject
if (!source.canTransform(null)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (logic.startsWith("MoveCounter")) {
return doMoveCounterLogic(ai, sa, ph);
@@ -295,8 +287,15 @@ public class CountersPutAi extends CountersAi {
if (willActivate && ph.getPhase().isBefore(PhaseType.MAIN2)) {
// don't use this for mana until after combat
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
return new AiAbilityDecision(25, AiPlayDecision.WaitForMain2);
}
return willActivate;
if (willActivate) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (logic.equals("ChargeToBestCMC")) {
return doChargeToCMCLogic(ai, sa);
} else if (logic.equals("ChargeToBestOppControlledCMC")) {
@@ -305,15 +304,11 @@ public class CountersPutAi extends CountersAi {
return SpecialCardAi.TheOneRing.consider(ai, sa);
}
if (!sa.metConditions() && sa.getSubAbility() == null) {
return false;
}
if (sourceName.equals("Feat of Resistance")) { // sub-ability should take precedence
CardCollection prot = ProtectAi.getProtectCreatures(ai, sa.getSubAbility());
if (!prot.isEmpty()) {
sa.getTargets().add(prot.get(0));
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}
@@ -321,13 +316,13 @@ public class CountersPutAi extends CountersAi {
CardCollection creatsYouCtrl = ai.getCreaturesInPlay();
List<Card> leastToughness = Aggregates.listWithMin(creatsYouCtrl, Card::getNetToughness);
if (leastToughness.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
// TODO If Creature that would be Bolstered for some reason is useless, also return False
}
if (sa.hasParam("Monstrosity") && source.isMonstrous()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// TODO handle proper calculation of X values based on Cost
@@ -341,8 +336,8 @@ public class CountersPutAi extends CountersAi {
Game game = ai.getGame();
Combat combat = game.getCombat();
if (!source.canReceiveCounters(CounterType.get(CounterEnumType.P1P1)) || source.getCounters(CounterEnumType.P1P1) > 0) {
return false;
if (!source.canReceiveCounters(CounterEnumType.P1P1) || source.getCounters(CounterEnumType.P1P1) > 0) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (combat != null && ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return doCombatAdaptLogic(source, amount, combat);
}
@@ -369,12 +364,12 @@ public class CountersPutAi extends CountersAi {
// This will "rewind" clockwork cards when they fall to 50% power or below, consider improving
if (curCtrs > Math.ceil(maxCtrs / 2.0)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
amount = Math.min(amount, maxCtrs - curCtrs);
if (amount <= 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -386,14 +381,14 @@ public class CountersPutAi extends CountersAi {
.mapToInt(Card::getCMC)
.max().orElse(0);
if (amount > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
// don't use it if no counters to add
if (amount <= 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if ("Polukranos".equals(logic)) {
@@ -420,20 +415,14 @@ public class CountersPutAi extends CountersAi {
}
}
if (!canSurvive) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
found = true;
break;
}
if (!found) {
return false;
}
}
if ("AtOppEOT".equals(logic)) {
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
return true;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -444,17 +433,19 @@ public class CountersPutAi extends CountersAi {
if (!ai.getGame().getStack().isEmpty() && !isSorcerySpeed(sa, ai)) {
// only evaluates case where all tokens are placed on a single target
if (sa.getMinTargets() < 2) {
if (ComputerUtilCard.canPumpAgainstRemoval(ai, sa)) {
AiAbilityDecision decision = ComputerUtilCard.canPumpAgainstRemoval(ai, sa);
if (decision.willingToPlay()) {
Card c = sa.getTargetCard();
if (sa.getTargets().size() > 1) {
sa.resetTargets();
sa.getTargets().add(c);
}
sa.addDividedAllocation(c, amount);
return true;
return decision;
} else {
if (!hasSacCost) { // for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return false;
if (!hasSacCost) {
// for Sacrifice costs, evaluate further to see if it's worth using the ability before the card dies
return decision;
}
}
}
@@ -498,7 +489,7 @@ public class CountersPutAi extends CountersAi {
}
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// Activate +Loyalty planeswalker abilities even if they have no target (e.g. Vivien of the Arkbow),
@@ -507,9 +498,9 @@ public class CountersPutAi extends CountersAi {
&& sa.isPwAbility()
&& sa.getPayCosts().hasOnlySpecificCostType(CostPutCounter.class)
&& sa.isTargetNumberValid()
&& sa.getTargets().size() == 0
&& sa.getTargets().isEmpty()
&& ai.getGame().getPhaseHandler().is(PhaseType.MAIN2, ai)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (sourceName.equals("Abzan Charm")) {
@@ -531,11 +522,11 @@ public class CountersPutAi extends CountersAi {
}
}
if (left == 0) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
sa.resetTargets();
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// target loop
@@ -543,7 +534,7 @@ public class CountersPutAi extends CountersAi {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -575,10 +566,9 @@ public class CountersPutAi extends CountersAi {
// check if other choice will already be played
increasesCharmOutcome = !choices.get(0).getTargets().isEmpty();
}
if (!source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
if (source != null && !source.isSpell() || increasesCharmOutcome // does not cost a card or can buff charm for no expense
|| ph.getTurn() - source.getTurnInZone() >= source.getGame().getPlayers().size() * 2) {
if (abCost == null || abCost == Cost.Zero
|| (ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai))) {
if (abCost == Cost.Zero || ph.is(PhaseType.END_OF_TURN) && ph.getPlayerTurn().isOpponentOf(ai)) {
// only use at opponent EOT unless it is free
choice = chooseBoonTarget(list, type);
}
@@ -592,7 +582,7 @@ public class CountersPutAi extends CountersAi {
if (choice == null) { // can't find anything left
if (!sa.isTargetNumberValid() || sa.getTargets().isEmpty()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -608,66 +598,65 @@ public class CountersPutAi extends CountersAi {
choice = null;
}
if (sa.getTargets().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
final List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
// Don't activate Curse abilities on my cards and non-curse abilities
// on my opponents
if (cards.isEmpty() || (cards.get(0).getController().isOpponentOf(ai) && !sa.isCurse())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.MissingNeededCards);
}
final int currCounters = cards.get(0).getCounters(CounterType.get(type));
final int currCounters = cards.get(0).getCounters(CounterType.getType(type));
// each non +1/+1 counter on the card is a 10% chance of not
// activating this ability.
if (!(type.equals("P1P1") || type.equals("M1M1") || type.equals("ICE")) && (MyRandom.getRandom().nextFloat() < (.1 * currCounters))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Instant +1/+1
if (type.equals("P1P1") && !isSorcerySpeed(sa, ai)) {
if (!hasSacCost && !(ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN) && abCost.isReusuableResource())) {
return false; // only if next turn and cost is reusable
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// Useless since the card already has the keyword (or for another reason)
if (ComputerUtil.isUselessCounter(CounterType.get(type), cards.get(0))) {
return false;
if (ComputerUtil.isUselessCounter(CounterType.getType(type), cards.get(0))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
boolean immediately = ComputerUtil.playImmediately(ai, sa);
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
return false;
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
if (immediately) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (!type.equals("P1P1") && !type.equals("M1M1") && !sa.hasParam("ActivationPhases")) {
// Don't use non P1P1/M1M1 counters before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !ComputerUtil.castSpellInMain1(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
if (ph.isPlayerTurn(ai) && !isSorcerySpeed(sa, ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
}
if (ComputerUtil.waitForBlocking(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(final SpellAbility sa, Player ai) {
boolean chance = true;
public AiAbilityDecision chkDrawback(final SpellAbility sa, Player ai) {
final Game game = ai.getGame();
Card choice = null;
final String type = sa.getParam("CounterType");
@@ -701,9 +690,9 @@ public class CountersPutAi extends CountersAi {
while (sa.canAddMoreTarget()) {
if (list.isEmpty()) {
if (!sa.isTargetNumberValid()
|| sa.getTargets().size() == 0) {
|| sa.getTargets().isEmpty()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
break;
}
@@ -724,9 +713,9 @@ public class CountersPutAi extends CountersAi {
}
if (choice == null) { // can't find anything left
if ((!sa.isTargetNumberValid()) || (sa.getTargets().size() == 0)) {
if ((!sa.isTargetNumberValid()) || (sa.getTargets().isEmpty())) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -741,15 +730,14 @@ public class CountersPutAi extends CountersAi {
}
}
return chance;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
final Card source = sa.getHostCard();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// boolean chance = true;
boolean preferred = true;
CardCollection list;
final String amountStr = sa.getParamOrDefault("CounterNum", "1");
@@ -770,9 +758,14 @@ public class CountersPutAi extends CountersAi {
}
if ("ChargeToBestCMC".equals(aiLogic)) {
return doChargeToCMCLogic(ai, sa) || mandatory;
} else if ("ChargeToBestOppControlledCMC".equals(aiLogic)) {
return doChargeToOppCtrlCMCLogic(ai, sa) || mandatory;
AiAbilityDecision decision = doChargeToCMCLogic(ai, sa);
if (decision.willingToPlay()) {
return decision;
} else if (mandatory) {
return new AiAbilityDecision(50, AiPlayDecision.MandatoryPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (!sa.usesTargeting()) {
@@ -801,7 +794,7 @@ public class CountersPutAi extends CountersAi {
sa.getTargetRestrictions().getAllCandidates(sa, true, true), Player.class));
if (playerList.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// try to choose player with less creatures
@@ -817,8 +810,9 @@ public class CountersPutAi extends CountersAi {
if (type.equals("P1P1")) {
nPump = amount;
}
if (FightAi.canFightAi(ai, sa, nPump, nPump)) {
return true;
AiAbilityDecision decision = FightAi.canFightAi(ai, sa, nPump, nPump);
if (decision.willingToPlay()) {
return decision;
}
}
@@ -839,7 +833,7 @@ public class CountersPutAi extends CountersAi {
if (mandatory) {
// When things are mandatory, gotta handle a little differently
if ((list.isEmpty() || !preferred) && sa.isTargetNumberValid()) {
return true;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (list.isEmpty() && preferred) {
@@ -859,7 +853,7 @@ public class CountersPutAi extends CountersAi {
if (list.isEmpty()) {
// Not mandatory, or the the list was regenerated and is still empty,
// so return whether or not we found enough targets
return sa.isTargetNumberValid();
return new AiAbilityDecision(sa.isTargetNumberValid() ? 100 : 0, sa.isTargetNumberValid() ? AiPlayDecision.WillPlay : AiPlayDecision.CantPlayAi);
}
Card choice = null;
@@ -912,7 +906,7 @@ public class CountersPutAi extends CountersAi {
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
@@ -967,8 +961,8 @@ public class CountersPutAi extends CountersAi {
protected Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer, Map<String, Object> params) {
// Bolster does use this
// TODO need more or less logic there?
final CounterType m1m1 = CounterType.get(CounterEnumType.M1M1);
final CounterType p1p1 = CounterType.get(CounterEnumType.P1P1);
final CounterType m1m1 = CounterEnumType.M1M1;
final CounterType p1p1 = CounterEnumType.P1P1;
// no logic if there is no options or no to choice
if (!isOptional && Iterables.size(options) <= 1) {
@@ -1089,8 +1083,8 @@ public class CountersPutAi extends CountersAi {
if (e instanceof Card) {
Card c = (Card) e;
if (c.getController().isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.M1M1);
if (options.contains(CounterEnumType.M1M1) && !c.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.M1M1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, c)) {
@@ -1107,12 +1101,12 @@ public class CountersPutAi extends CountersAi {
} else if (e instanceof Player) {
Player p = (Player) e;
if (p.isOpponentOf(ai)) {
if (options.contains(CounterType.get(CounterEnumType.POISON))) {
return CounterType.get(CounterEnumType.POISON);
if (options.contains(CounterEnumType.POISON)) {
return CounterEnumType.POISON;
}
} else {
if (options.contains(CounterType.get(CounterEnumType.EXPERIENCE))) {
return CounterType.get(CounterEnumType.EXPERIENCE);
if (options.contains(CounterEnumType.EXPERIENCE)) {
return CounterEnumType.EXPERIENCE;
}
}
@@ -1120,7 +1114,7 @@ public class CountersPutAi extends CountersAi {
return Iterables.getFirst(options, null);
}
private boolean doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
private AiAbilityDecision doMoveCounterLogic(final Player ai, SpellAbility sa, PhaseHandler ph) {
// Spikes (Tempest)
// Try not to do it unless at the end of opponent's turn or the creature is threatened
@@ -1133,7 +1127,7 @@ public class CountersPutAi extends CountersAi {
|| (combat.isBlocking(source) && ComputerUtilCombat.blockerWouldBeDestroyed(ai, source, combat) && !ComputerUtilCombat.willKillAtLeastOne(ai, source, combat))));
if (!(threatened || (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == ai))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
CardCollection targets = CardLists.getTargetableCards(ai.getCreaturesInPlay(), sa);
@@ -1151,45 +1145,45 @@ public class CountersPutAi extends CountersAi {
if (bestTgt != null) {
sa.getTargets().add(bestTgt);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
private boolean doCombatAdaptLogic(Card source, int amount, Combat combat) {
private AiAbilityDecision doCombatAdaptLogic(Card source, int amount, Combat combat) {
if (combat.isAttacking(source)) {
if (!combat.isBlocked(source)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
for (Card blockedBy : combat.getBlockers(source)) {
if (blockedBy.getNetToughness() > source.getNetPower()
&& blockedBy.getNetToughness() <= source.getNetPower() + amount) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
int totBlkPower = Aggregates.sum(combat.getBlockers(source), Card::getNetPower);
if (source.getNetToughness() <= totBlkPower
&& source.getNetToughness() + amount > totBlkPower) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}
} else if (combat.isBlocking(source)) {
for (Card blocked : combat.getAttackersBlockedBy(source)) {
if (blocked.getNetToughness() > source.getNetPower()
&& blocked.getNetToughness() <= source.getNetPower() + amount) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.ImpactCombat);
}
}
int totAtkPower = Aggregates.sum(combat.getAttackersBlockedBy(source), Card::getNetPower);
if (source.getNetToughness() <= totAtkPower
&& source.getNetToughness() + amount > totAtkPower) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
@@ -1200,7 +1194,7 @@ public class CountersPutAi extends CountersAi {
return max;
}
private boolean doChargeToCMCLogic(Player ai, SpellAbility sa) {
private AiAbilityDecision doChargeToCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
CardCollectionView ownLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.CREATURES);
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
@@ -1215,10 +1209,14 @@ public class CountersPutAi extends CountersAi {
optimalCMC = cmc;
}
}
return numCtrs < optimalCMC;
if (numCtrs < optimalCMC) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
private boolean doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
private AiAbilityDecision doChargeToOppCtrlCMCLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
CardCollectionView oppInPlay = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield), CardPredicates.NONLAND_PERMANENTS);
int numCtrs = source.getCounters(CounterEnumType.CHARGE);
@@ -1232,6 +1230,12 @@ public class CountersPutAi extends CountersAi {
optimalCMC = cmc;
}
}
return numCtrs < optimalCMC;
if (numCtrs < optimalCMC) {
// If the AI has less counters than the optimal CMC, it should play the ability.
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// If the AI has enough counters or more than the optimal CMC, it should not play the ability.
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}

View File

@@ -1,12 +1,13 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -15,17 +16,15 @@ import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
public class CountersPutAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
List<Card> hList;
List<Card> cList;
@@ -44,28 +43,9 @@ public class CountersPutAllAi extends SpellAbilityAi {
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source, sa);
}
if (abCost != null) {
// AI currently disabled for these costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 8, sa)) {
return false;
}
if (!ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
return false;
}
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
}
if (logic.equals("AtEOTOrBlock")) {
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false;
}
} else if (logic.equals("AtOppEOT")) {
if (!(ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && ai.getGame().getPhaseHandler().getNextTurn() == ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
}
@@ -88,26 +68,23 @@ public class CountersPutAllAi extends SpellAbilityAi {
amount = AbilityUtils.calculateAmount(source, amountStr, sa);
}
// prevent run-away activations - first time will always return true
boolean chance = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
if (curse) {
if (type.equals("M1M1")) {
final List<Card> killable = CardLists.filter(hList, c -> c.getNetToughness() <= amount);
if (!(killable.size() > 2)) {
return false;
if (killable.size() <= 2) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// make sure compy doesn't harm his stuff more than human's
// stuff
if (cList.size() > hList.size()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
// human has more things that will benefit, don't play
if (hList.size() >= cList.size()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
//Check for cards that could profit from the ability
@@ -125,21 +102,21 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
}
if (!combatants) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
if (playReusable(ai, sa)) {
return chance;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return ((MyRandom.getRandom().nextFloat() < .6667) && chance);
return super.checkApiLogic(ai, sa);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return canPlayAI(ai, sa);
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return canPlay(ai, sa);
}
/* (non-Javadoc)
* @see forge.card.ability.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.card.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
@@ -150,7 +127,7 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (sa.usesTargeting()) {
List<Player> players = Lists.newArrayList();
if (!sa.isCurse()) {
@@ -168,11 +145,23 @@ public class CountersPutAllAi extends SpellAbilityAi {
preferred = (sa.isCurse() && p.isOpponentOf(aiPlayer)) || (!sa.isCurse() && p == aiPlayer);
sa.resetTargets();
sa.getTargets().add(p);
return preferred || mandatory;
if (preferred) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
}
return mandatory || canPlayAI(aiPlayer, sa);
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(aiPlayer, sa);
}
}

View File

@@ -18,9 +18,7 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
@@ -52,9 +50,13 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (sa.usesTargeting()) {
return doTgt(ai, sa, false);
if (doTgt(ai, sa, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
return super.checkApiLogic(ai, sa);
}
@@ -180,11 +182,27 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
return doTgt(ai, sa, mandatory);
if (doTgt(ai, sa, mandatory)) {
// if we can target, then we can play it
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
// if we can't target, then we can't play it
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (mandatory) {
// if mandatory, just play it
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
// if not mandatory, check if we can play it
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return mandatory;
}
/*
@@ -200,18 +218,18 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
Card tgt = (Card) params.get("Target");
// planeswalker has high priority for loyalty counters
if (tgt.isPlaneswalker() && options.contains(CounterType.get(CounterEnumType.LOYALTY))) {
return CounterType.get(CounterEnumType.LOYALTY);
if (tgt.isPlaneswalker() && options.contains(CounterEnumType.LOYALTY)) {
return CounterEnumType.LOYALTY;
}
if (tgt.getController().isOpponentOf(ai)) {
// creatures with BaseToughness below or equal zero might be
// killed if their counters are removed
if (tgt.isCreature() && tgt.getBaseToughness() <= 0) {
if (options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterType.get(CounterEnumType.P1P1);
} else if (options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterType.get(CounterEnumType.M1M1);
if (options.contains(CounterEnumType.P1P1)) {
return CounterEnumType.P1P1;
} else if (options.contains(CounterEnumType.M1M1)) {
return CounterEnumType.M1M1;
}
}
@@ -223,17 +241,17 @@ public class CountersPutOrRemoveAi extends SpellAbilityAi {
}
} else {
// this counters are treat first to be removed
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterType.get(CounterEnumType.ICE))) {
if ("Dark Depths".equals(tgt.getName()) && options.contains(CounterEnumType.ICE)) {
CardCollectionView marit = ai.getCardsIn(ZoneType.Battlefield, "Marit Lage");
boolean maritEmpty = marit.isEmpty() || Iterables.contains(marit, (Predicate<Card>) Card::ignoreLegendRule);
if (maritEmpty) {
return CounterType.get(CounterEnumType.ICE);
return CounterEnumType.ICE;
}
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterType.get(CounterEnumType.P1P1))) {
return CounterType.get(CounterEnumType.P1P1);
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterType.get(CounterEnumType.M1M1))) {
return CounterType.get(CounterEnumType.M1M1);
} else if (tgt.hasKeyword(Keyword.UNDYING) && options.contains(CounterEnumType.P1P1)) {
return CounterEnumType.P1P1;
} else if (tgt.hasKeyword(Keyword.PERSIST) && options.contains(CounterEnumType.M1M1)) {
return CounterEnumType.M1M1;
}
// fallback logic, select positive counter to add more

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
@@ -23,14 +25,6 @@ import java.util.function.Predicate;
public class CountersRemoveAi extends SpellAbilityAi {
@Override
protected boolean canPlayWithoutRestrict(final Player ai, final SpellAbility sa) {
if ("Always".equals(sa.getParam("AILogic"))) {
return true;
}
return super.canPlayWithoutRestrict(ai, sa);
}
/*
* (non-Javadoc)
*
@@ -48,24 +42,6 @@ public class CountersRemoveAi extends SpellAbilityAi {
return super.checkPhaseRestrictions(ai, sa, ph);
}
/*
* (non-Javadoc)
*
* @see
* forge.ai.SpellAbilityAi#checkPhaseRestrictions(forge.game.player.Player,
* forge.game.spellability.SpellAbility, forge.game.phase.PhaseHandler,
* java.lang.String)
*/
@Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph, String logic) {
if ("EndOfOpponentsTurn".equals(logic)) {
if (!ph.is(PhaseType.END_OF_TURN) || !ph.getNextTurn().equals(ai)) {
return false;
}
}
return super.checkPhaseRestrictions(ai, sa, ph, logic);
}
/*
* (non-Javadoc)
*
@@ -73,7 +49,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
* forge.game.spellability.SpellAbility)
*/
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final String type = sa.getParam("CounterType");
if (sa.usesTargeting()) {
@@ -83,14 +59,14 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!type.matches("Any") && !type.matches("All")) {
final int currCounters = sa.getHostCard().getCounters(CounterType.getType(type));
if (currCounters < 1) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
return super.checkApiLogic(ai, sa);
}
private boolean doTgt(Player ai, SpellAbility sa, boolean mandatory) {
private AiAbilityDecision doTgt(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final Game game = ai.getGame();
@@ -103,7 +79,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(game.getCardsIn(tgt.getZone()), sa);
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// Filter AI-specific targets if provided
@@ -121,7 +97,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardPredicates.hasCounter(CounterEnumType.ICE, 3));
if (!depthsList.isEmpty()) {
sa.getTargets().add(depthsList.getFirst());
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -134,7 +110,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!planeswalkerList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestPlaneswalkerAI(planeswalkerList));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (type.matches("Any")) {
// variable amount for Hex Parasite
@@ -144,7 +120,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (manaLeft == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
amount = manaLeft;
xPay = true;
@@ -166,7 +142,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) {
sa.setXManaCostPaid(ice);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
@@ -185,7 +161,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) {
sa.setXManaCostPaid(best.getCurrentLoyalty());
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// some rules only for amount = 1
@@ -202,7 +178,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiM1M1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiM1M1List));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// do as P1P1 part
@@ -211,7 +187,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiUndyingList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiUndyingList));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// TODO stun counters with canRemoveCounters check
@@ -222,7 +198,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardPredicates.hasCounter(CounterEnumType.P1P1));
if (!oppP1P1List.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(oppP1P1List));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// fallback to remove any counter from opponent
@@ -234,7 +210,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
for (final CounterType aType : best.getCounters().keySet()) {
if (!ComputerUtil.isNegativeCounter(aType, best)) {
sa.getTargets().add(best);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
@@ -255,7 +231,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!aiList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (type.equals("P1P1")) {
// no special amount for that one yet
@@ -273,7 +249,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
if (!aiList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(aiList));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -287,7 +263,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!oppList.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstCreatureAI(oppList));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
} else if (type.equals("TIME")) {
@@ -298,7 +274,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
final int manaLeft = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (manaLeft == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
amount = manaLeft;
xPay = true;
@@ -316,7 +292,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (xPay) {
sa.setXManaCostPaid(timeCount);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
if (mandatory) {
@@ -325,7 +301,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
CardCollection adaptCreats = CardLists.filter(list, CardPredicates.hasKeyword(Keyword.ADAPT));
if (!adaptCreats.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstAI(adaptCreats));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// Outlast nice target
@@ -336,26 +312,27 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (!betterTargets.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getWorstAI(betterTargets));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
sa.getTargets().add(ComputerUtilCard.getWorstAI(outlastCreats));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
sa.getTargets().add(ComputerUtilCard.getWorstAI(list));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
return doTgt(aiPlayer, sa, mandatory);
}
return mandatory;
return mandatory ? new AiAbilityDecision(100, AiPlayDecision.MandatoryPlay)
: new AiAbilityDecision(0, AiPlayDecision.CantPlaySa);
}
/*
@@ -369,8 +346,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
GameEntity target = (GameEntity) params.get("Target");
CounterType type = (CounterType) params.get("CounterType");
if (target instanceof Card) {
Card targetCard = (Card) target;
if (target instanceof Card targetCard) {
if (targetCard.getController().isOpponentOf(player)) {
return !ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
} else {
@@ -381,8 +357,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
return ComputerUtil.isNegativeCounter(type, targetCard) ? max : min;
}
} else if (target instanceof Player) {
Player targetPlayer = (Player) target;
} else if (target instanceof Player targetPlayer) {
if (targetPlayer.isOpponentOf(player)) {
return !type.is(CounterEnumType.POISON) ? max : min;
} else {
@@ -409,7 +384,7 @@ public class CountersRemoveAi extends SpellAbilityAi {
if (targetCard.getController().isOpponentOf(ai)) {
// if its a Planeswalker try to remove Loyality first
if (targetCard.isPlaneswalker()) {
return CounterType.get(CounterEnumType.LOYALTY);
return CounterEnumType.LOYALTY;
}
for (CounterType type : options) {
if (!ComputerUtil.isNegativeCounter(type, targetCard)) {
@@ -417,10 +392,10 @@ public class CountersRemoveAi extends SpellAbilityAi {
}
}
} else {
if (options.contains(CounterType.get(CounterEnumType.M1M1)) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterType.get(CounterEnumType.M1M1);
} else if (options.contains(CounterType.get(CounterEnumType.P1P1)) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterType.get(CounterEnumType.P1P1);
if (options.contains(CounterEnumType.M1M1) && targetCard.hasKeyword(Keyword.PERSIST)) {
return CounterEnumType.M1M1;
} else if (options.contains(CounterEnumType.P1P1) && targetCard.hasKeyword(Keyword.UNDYING)) {
return CounterEnumType.P1P1;
}
for (CounterType type : options) {
if (ComputerUtil.isNegativeCounter(type, targetCard)) {

View File

@@ -5,38 +5,24 @@ import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.cost.Cost;
import forge.game.keyword.Keyword;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.function.Predicate;
public class DamageAllAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Card source = sa.getHostCard();
// prevent run-away activations - first time will always return true
if (MyRandom.getRandom().nextFloat() > Math.pow(.9, sa.getActivationsThisTurn())) {
return false;
}
// abCost stuff that should probably be centralized...
final Cost abCost = sa.getPayCosts();
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}
// wait until stack is empty (prevents duplicate kills)
if (!ai.getGame().getStack().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
}
int x = -1;
@@ -51,11 +37,15 @@ public class DamageAllAi extends SpellAbilityAi {
if (x == -1) {
if (determineOppToKill(ai, sa, source, dmg) != null) {
// we already know we can kill a player, so go for it
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// look for other value in this (damaging creatures or
// creatures + player, e.g. Pestilence, etc.)
return evaluateDamageAll(ai, sa, source, dmg) > 0;
if (evaluateDamageAll(ai, sa, source, dmg) > 0) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
int best = -1, best_x = -1;
Player bestOpp = determineOppToKill(ai, sa, source, x);
@@ -81,9 +71,9 @@ public class DamageAllAi extends SpellAbilityAi {
if (sa.getSVar(damage).equals("Count$xPaid")) {
sa.setXManaCostPaid(best_x);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -143,9 +133,9 @@ public class DamageAllAi extends SpellAbilityAi {
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
// When using Pestilence to hurt players, do it at
// the end of the opponent's turn only
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& (ai.getGame().getNonactivePlayers().contains(ai)))))
if (!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))
|| (ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)))
// Need further improvement : if able to kill immediately with repeated activations, do not wait
// for phases! Will also need to implement considering repeated activations for killed creatures!
// || (ai.sa.getPayCosts(). ??? )
@@ -185,7 +175,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
final Card source = sa.getHostCard();
final String validP = sa.getParamOrDefault("ValidPlayers", "");
@@ -211,21 +201,21 @@ public class DamageAllAi extends SpellAbilityAi {
}
// Don't get yourself killed
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// if we can kill human, do it
if ((validP.equals("Player") || validP.equals("Opponent") || validP.contains("Targeted"))
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) > ComputerUtilCard
.evaluateCreatureList(humanList)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -258,7 +248,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final String validP = sa.getParamOrDefault("ValidPlayers", "");
@@ -287,24 +277,24 @@ public class DamageAllAi extends SpellAbilityAi {
// If it's not mandatory check a few things
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// Don't get yourself killed
if (validP.equals("Player") && (ai.getLife() <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false))) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// if we can kill human, do it
if ((validP.equals("Player") || validP.contains("Opponent") || validP.contains("Targeted"))
&& (enemy.getLife() <= ComputerUtilCombat.predictDamageTo(enemy, dmg, source, false))) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (!computerList.isEmpty() && ComputerUtilCard.evaluateCreatureList(computerList) + 50 >= ComputerUtilCard
.evaluateCreatureList(humanList)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -38,7 +38,7 @@ import java.util.Map;
public class DamageDealAi extends DamageAiBase {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
final SpellAbility root = sa.getRootAbility();
final String damage = sa.getParam("NumDmg");
Card source = sa.getHostCard();
@@ -65,15 +65,19 @@ public class DamageDealAi extends DamageAiBase {
continue; // in case the calculation gets messed up somewhere
}
root.setSVar("EnergyToPay", "Number$" + dmg);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.getSVar(damage).equals("Count$xPaid")) {
// Life Drain
if ("XLifeDrain".equals(logic)) {
return doXLifeDrainLogic(ai, sa);
if (doXLifeDrainLogic(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// Set PayX here to maximum value.
@@ -83,11 +87,15 @@ public class DamageDealAi extends DamageAiBase {
dmg--; // the card will be spent casting the spell, so actual damage is 1 less
}
}
return damageTargetAI(ai, sa, dmg, true);
if (damageTargetAI(ai, sa, dmg, true)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
@@ -95,7 +103,7 @@ public class DamageDealAi extends DamageAiBase {
final String damage = sa.getParam("NumDmg");
int dmg = calculateDamageAmount(sa, source, damage);
if (damage.equals("X") || source.getSVar("X").equals("Count$xPaid")) {
if (damage.equals("X") || (dmg == 0 && source.getSVar("X").equals("Count$xPaid"))) {
if (sa.getSVar("X").equals("Count$xPaid") || sa.getSVar(damage).equals("Count$xPaid")) {
dmg = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
@@ -108,7 +116,7 @@ public class DamageDealAi extends DamageAiBase {
boolean inDanger = ComputerUtil.aiLifeInDanger(ai, false, 0);
boolean isLethal = sa.usesTargeting() && sa.getTargetRestrictions().canTgtPlayer() && dmg >= ai.getWeakestOpponent().getLife() && !ai.getWeakestOpponent().cantLoseForZeroOrLessLife();
if (dmg < threshold && ai.getGame().getPhaseHandler().getTurn() / 2 < threshold && !inDanger && !isLethal) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -134,10 +142,10 @@ public class DamageDealAi extends DamageAiBase {
if (shouldTgtP(ai, sa, maxDmg, false)) {
sa.resetTargets();
sa.getTargets().add(maxDamaged);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
}
@@ -154,7 +162,7 @@ public class DamageDealAi extends DamageAiBase {
if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
for (Card potentialAtkr : ai.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -175,16 +183,24 @@ public class DamageDealAi extends DamageAiBase {
* Mostly used to ping the player with remaining counters. The issue with
* stacked effects might appear here.
*/
return damageTargetAI(ai, sa, n, true);
if (damageTargetAI(ai, sa, n, true)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
/*
* Only ping when stack is clear to avoid hassle of evaluating stacked effects
* like protection/pumps or over-killing target.
*/
return ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false);
if (ai.getGame().getStack().isEmpty() && damageTargetAI(ai, sa, n, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.StackNotEmpty);
}
}
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if ("NinThePainArtist".equals(logic)) {
// Make sure not to mana lock ourselves + make the opponent draw cards into an immediate discard
@@ -193,11 +209,15 @@ public class DamageDealAi extends DamageAiBase {
if (doTarget) {
Card tgt = sa.getTargetCard();
if (tgt != null) {
return ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController();
if (ai.getGame().getPhaseHandler().getPlayerTurn() == tgt.getController()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
}
}
return false;
}
return new AiAbilityDecision(0, AiPlayDecision.WaitForEndOfTurn);
}
if (sourceName.equals("Sorin, Grim Nemesis")) {
@@ -209,35 +229,35 @@ public class DamageDealAi extends DamageAiBase {
continue; // in case the calculation gets messed up somewhere
}
sa.setXManaCostPaid(dmg);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (dmg <= 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// temporarily disabled until better AI
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
if (!ComputerUtilCost.checkRemoveCounterCost(abCost, source, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
if ("DiscardLands".equals(sa.getParam("AILogic")) && !ComputerUtilCost.checkDiscardCost(ai, abCost, source, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Try to chain damage/debuff effects
@@ -248,13 +268,13 @@ public class DamageDealAi extends DamageAiBase {
int extraDmg = chainDmg.getValue();
boolean willTargetIfChained = damageTargetAI(ai, sa, dmg + extraDmg, false);
if (!willTargetIfChained) {
return false; // won't play it even in chain
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed); // won't play it even in chain
} else if (willTargetIfChained && chainDmg.getKey().getApi() == ApiType.Pump && sa.getTargets().isTargetingAnyPlayer()) {
// we're trying to chain a pump spell to a damage spell targeting a player, that won't work
// so run an additional check to ensure that we want to cast the current spell separately
sa.resetTargets();
if (!damageTargetAI(ai, sa, dmg, false)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
// we are about to decide to play this damage spell; if there's something chained to it, reserve mana for
@@ -264,7 +284,7 @@ public class DamageDealAi extends DamageAiBase {
}
} else if (!damageTargetAI(ai, sa, dmg, false)) {
// simple targeting when there is no spell chaining plan
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if ((damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) ||
@@ -288,10 +308,12 @@ public class DamageDealAi extends DamageAiBase {
if ("DiscardCMCX".equals(sa.getParam("AILogic"))) {
final int cmc = sa.getXManaCostPaid();
return ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc));
if (!ai.getZone(ZoneType.Hand).contains(CardPredicates.hasCMC(cmc))) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/**
@@ -932,14 +954,14 @@ public class DamageDealAi extends DamageAiBase {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card source = sa.getHostCard();
final String damage = sa.getParam("NumDmg");
int dmg = calculateDamageAmount(sa, source, damage);
// Remove all damage
if (sa.hasParam("Remove")) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid")) {
@@ -950,10 +972,18 @@ public class DamageDealAi extends DamageAiBase {
if (!sa.usesTargeting()) {
// If it's not mandatory check a few things
return mandatory || damageChooseNontargeted(ai, sa, dmg);
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (damageChooseNontargeted(ai, sa, dmg)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
if (!damageChoosingTargets(ai, sa, sa.getTargetRestrictions(), dmg, mandatory, true) && !mandatory) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (damage.equals("X") && sa.getSVar(damage).equals("Count$xPaid") && !sa.isDividedAsYouChoose()) {
@@ -976,7 +1006,7 @@ public class DamageDealAi extends DamageAiBase {
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private static int calculateDamageAmount(SpellAbility sa, Card source, String damage) {

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpecialCardAi;
import forge.game.ability.AbilityUtils;
import forge.game.player.Player;
@@ -14,7 +16,7 @@ public class DamageEachAi extends DamageAiBase {
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
final String logic = sa.getParam("AILogic");
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
@@ -22,30 +24,41 @@ public class DamageEachAi extends DamageAiBase {
if (sa.usesTargeting() && weakestOpp != null) {
if ("MadSarkhanUltimate".equals(logic) && !SpecialCardAi.SarkhanTheMad.considerUltimate(ai, sa, weakestOpp)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.resetTargets();
if (weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife()) {
sa.getTargets().add(weakestOpp);
return weakestOpp.canLoseLife() && !weakestOpp.cantLoseForZeroOrLessLife();
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
final String damage = sa.getParam("NumDmg");
final int iDmg = AbilityUtils.calculateAmount(sa.getHostCard(), damage, sa);
return shouldTgtP(ai, sa, iDmg, false);
if (shouldTgtP(ai, sa, iDmg, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
// check AI life before playing this drawback?
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
return mandatory || canPlayAI(ai, sa);
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return canPlay(ai, sa);
}
}

View File

@@ -1,15 +1,11 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.GameObject;
import forge.game.ability.AbilityUtils;
import forge.game.card.*;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -24,18 +20,12 @@ import java.util.List;
public class DamagePreventAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card hostCard = sa.getHostCard();
final Game game = ai.getGame();
final Combat combat = game.getCombat();
boolean chance = false;
final Cost cost = sa.getPayCosts();
if (!willPayCosts(ai, sa, cost, hostCard)) {
return false;
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
// As far as I can tell these Defined Cards will only have one of them
@@ -70,7 +60,7 @@ public class DamagePreventAi extends SpellAbilityAi {
chance = flag;
} else { // if nothing on the stack, and it's not declare
// blockers. no need to prevent
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} // non-targeted
@@ -120,7 +110,7 @@ public class DamagePreventAi extends SpellAbilityAi {
targetables = CardLists.getTargetableCards(targetables, sa);
if (targetables.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
final CardCollection combatants = CardLists.filter(targetables, CardPredicates.CREATURES);
ComputerUtilCard.sortByEvaluateCreature(combatants);
@@ -137,11 +127,15 @@ public class DamagePreventAi extends SpellAbilityAi {
sa.addDividedAllocation(sa.getTargets().get(0), AbilityUtils.calculateAmount(hostCard, sa.getParam("Amount"), sa));
}
return chance;
if (chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
boolean chance = false;
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt == null) {
@@ -151,7 +145,11 @@ public class DamagePreventAi extends SpellAbilityAi {
chance = preventDamageMandatoryTarget(ai, sa, mandatory);
}
return chance;
if (chance) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.StopRunawayActivations);
}
}
/**

View File

@@ -1,5 +1,7 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -11,24 +13,34 @@ import java.util.Map;
public class DayTimeAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
PhaseHandler ph = aiPlayer.getGame().getPhaseHandler();
if ((sa.getHostCard().isCreature() && sa.getPayCosts().hasTapCost()) || sa.getPayCosts().hasManaCost()) {
// If it involves a cost that may put us at a disadvantage, better activate before own turn if possible
if (!isSorcerySpeed(sa, aiPlayer)) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer;
if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn() == aiPlayer) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return ph.is(PhaseType.MAIN2, aiPlayer); // Give other things a chance to be cast (e.g. Celestus)
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
} else {
if (ph.is(PhaseType.MAIN2, aiPlayer)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return true; // TODO: more logic if it's ever a bad idea to trigger this (when non-mandatory)
protected AiAbilityDecision doTriggerNoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override

View File

@@ -1,10 +1,7 @@
package forge.ai.ability;
import com.google.common.collect.Lists;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -26,27 +23,27 @@ import java.util.List;
public class DebuffAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision canPlay(final Player ai, final SpellAbility sa) {
// if there is no target and host card isn't in play, don't activate
final Card source = sa.getHostCard();
final Game game = ai.getGame();
if (!sa.usesTargeting() && !source.isInPlay()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final Cost cost = sa.getPayCosts();
// temporarily disabled until AI is improved
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 40, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAfford);
}
final PhaseHandler ph = game.getPhaseHandler();
@@ -58,7 +55,7 @@ public class DebuffAi extends SpellAbilityAi {
// Instant-speed pumps should not be cast outside of combat when the
// stack is empty, unless there are specific activation phase requirements
if (!isSorcerySpeed(sa, ai) && !sa.hasParam("ActivationPhases")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.AnotherTime);
}
}
@@ -66,7 +63,7 @@ public class DebuffAi extends SpellAbilityAi {
List<Card> cards = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
final Combat combat = game.getCombat();
return cards.stream().anyMatch(c -> {
if (cards.stream().anyMatch(c -> {
if (c.getController().equals(sa.getActivatingPlayer()) || combat == null)
return false;
@@ -75,21 +72,34 @@ public class DebuffAi extends SpellAbilityAi {
}
// don't add duplicate negative keywords
return sa.hasParam("Keywords") && c.hasAnyKeyword(Arrays.asList(sa.getParam("Keywords").split(" & ")));
});
})) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
if (!sa.usesTargeting()) {
// TODO - copied from AF_Pump.pumpDrawbackAI() - what should be here?
} else {
return debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false);
if (debuffTgtAI(ai, sa, sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : null, false)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} // debuffDrawbackAI()
/**
@@ -234,18 +244,24 @@ public class DebuffAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final List<String> kws = sa.hasParam("Keywords") ? Arrays.asList(sa.getParam("Keywords").split(" & ")) : new ArrayList<>();
if (!sa.usesTargeting()) {
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
return debuffTgtAI(ai, sa, kws, mandatory);
if (debuffTgtAI(ai, sa, kws, mandatory)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -15,43 +15,56 @@ import forge.game.zone.ZoneType;
public class DelayedTriggerAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
if ("Always".equals(sa.getParam("AILogic"))) {
// TODO: improve ai
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
trigsa.setActivatingPlayer(ai);
if (trigsa instanceof AbilitySub) {
return SpellApiToAi.Converter.get(trigsa).chkDrawbackWithSubs(ai, (AbilitySub)trigsa);
} else {
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
if (decision == AiPlayDecision.WillPlay) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
trigsa.setActivatingPlayer(ai);
if (!sa.hasParam("OptionalDecider")) {
return aic.doTrigger(trigsa, true);
if (aic.doTrigger(trigsa, true)) {
// If the trigger is mandatory, we can play it
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"));
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
if (aic.doTrigger(trigsa, !sa.getParam("OptionalDecider").equals("You"))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision canPlay(Player ai, SpellAbility sa) {
// Card-specific logic
String logic = sa.getParamOrDefault("AILogic", "");
if (logic.equals("SpellCopy")) {
@@ -90,9 +103,9 @@ public class DelayedTriggerAi extends SpellAbilityAi {
});
if (count == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (logic.equals("NarsetRebound")) {
// should be done in Main2, but it might broke for other cards
//if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
@@ -125,10 +138,10 @@ public class DelayedTriggerAi extends SpellAbilityAi {
});
if (count == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (logic.equals("SaveCreature")) {
CardCollection ownCreatures = ai.getCreaturesInPlay();
@@ -142,19 +155,25 @@ public class DelayedTriggerAi extends SpellAbilityAi {
if (!ownCreatures.isEmpty()) {
sa.getTargets().add(ComputerUtilCard.getBestAI(ownCreatures));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// Generic logic
SpellAbility trigsa = sa.getAdditionalAbility("Execute");
if (trigsa == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
trigsa.setActivatingPlayer(ai);
return AiPlayDecision.WillPlay == ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
AiPlayDecision decision = ((PlayerControllerAi)ai.getController()).getAi().canPlaySa(trigsa);
if (decision == AiPlayDecision.WillPlay) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}

View File

@@ -20,7 +20,7 @@ import forge.util.collect.FCollectionView;
public class DestroyAi extends SpellAbilityAi {
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return checkApiLogic(ai, sa);
}
@@ -103,36 +103,27 @@ public class DestroyAi extends SpellAbilityAi {
}
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
final boolean noRegen = sa.hasParam("NoRegen");
final String logic = sa.getParam("AILogic");
CardCollection list;
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
// Targeting
if (sa.usesTargeting()) {
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if ("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid")) {
int xPay = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
sa.getRootAbility().setXManaCostPaid(xPay);
}
// Assume there where already enough targets chosen by AI Logic Above
if (sa.hasParam("AILogic") && !sa.canAddMoreTarget() && sa.isTargetNumberValid()) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// reset targets before AI Logic part
sa.resetTargets();
int maxTargets;
if (sa.getRootAbility().costHasManaX()) {
// If there's X in payment costs and it's tied to targeting, make sure we set the XManaCostPaid first
// (e.g. Heliod's Intervention)
if (sa.getRootAbility().costHasManaX() ||
("X".equals(sa.getTargetRestrictions().getMinTargets()) && sa.getSVar("X").equals("Count$xPaid"))) {
// TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement.
maxTargets = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
// need to set XPaid to get the right number for
@@ -145,23 +136,22 @@ public class DestroyAi extends SpellAbilityAi {
if (maxTargets == 0) {
// can't afford X or otherwise target anything
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantAffordX);
}
if (sa.hasParam("TargetingPlayer")) {
Player targetingPlayer = AbilityUtils.getDefinedPlayers(source, sa.getParam("TargetingPlayer"), sa).get(0);
sa.setTargetingPlayer(targetingPlayer);
return targetingPlayer.getController().chooseTargetsFor(sa);
if (targetingPlayer.getController().chooseTargetsFor(sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
// AI doesn't destroy own cards if it isn't defined in AI logic
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
if ("FatalPush".equals(logic)) {
final int cmcMax = ai.hasRevolt() ? 4 : 2;
list = CardLists.filter(list, CardPredicates.lessCMC(cmcMax));
}
// Filter AI-specific targets if provided
list = ComputerUtil.filterAITgts(sa, ai, list, true);
list = CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE);
@@ -206,7 +196,7 @@ public class DestroyAi extends SpellAbilityAi {
// Try to avoid targeting creatures that are dead on board
list = ComputerUtil.filterCreaturesThatWillDieThisTurn(ai, list, sa);
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
// target loop
@@ -216,10 +206,12 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection originalList = new CardCollection(list);
boolean mustTargetFiltered = StaticAbilityMustTarget.filterMustTargetCards(ai, list, sa);
list = CardLists.canSubsequentlyTarget(list, sa);
if (list.isEmpty()) {
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -233,7 +225,7 @@ public class DestroyAi extends SpellAbilityAi {
if ("OppDestroyYours".equals(logic)) {
Card aiBest = ComputerUtilCard.getBestCreatureAI(ai.getCreaturesInPlay());
if (ComputerUtilCard.evaluateCreature(aiBest) > ComputerUtilCard.evaluateCreature(choice) - 40) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else if (CardLists.getNotType(list, "Land").isEmpty()) {
@@ -242,7 +234,7 @@ public class DestroyAi extends SpellAbilityAi {
if ("LandForLand".equals(logic) || "GhostQuarter".equals(logic)) {
// Strip Mine, Wasteland - cut short if the relevant logic fails
if (!doLandForLandRemovalLogic(sa, ai, choice, logic)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
@@ -252,14 +244,14 @@ public class DestroyAi extends SpellAbilityAi {
//option to hold removal instead only applies for single targeted removal
if (!sa.isTrigger() && sa.getMaxTargets() == 1) {
if (choice == null || !ComputerUtilCard.useRemovalNow(sa, choice, 0, ZoneType.Graveyard)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
if (choice == null) { // can't find anything left
if (!sa.isMinTargetChosen() || sa.isZeroTargets()) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
// TODO is this good enough? for up to amounts?
break;
@@ -275,6 +267,7 @@ public class DestroyAi extends SpellAbilityAi {
choice = aura;
}
}
// TODO What about stolen permanents we're getting back at the end of the turn?
}
}
@@ -284,8 +277,10 @@ public class DestroyAi extends SpellAbilityAi {
}
list.remove(choice);
if (sa.canTarget(choice)) {
sa.getTargets().add(choice);
}
}
} else if (sa.hasParam("Defined")) {
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if ("WillSkipTurn".equals(logic) && (source.getController().equals(ai)
@@ -293,22 +288,22 @@ public class DestroyAi extends SpellAbilityAi {
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|| ai.getLife() <= 5)) {
// Basic ai logic for Lethal Vapors
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if ("Always".equals(logic)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (list.isEmpty()
|| !CardLists.filterControlledBy(list, ai).isEmpty()
|| CardLists.getNotKeyword(list, Keyword.INDESTRUCTIBLE).isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final boolean noRegen = sa.hasParam("NoRegen");
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -316,7 +311,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list = CardLists.getTargetableCards(ai.getGame().getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty() || list.size() < sa.getMinTargets()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Try to avoid targeting creatures that are dead on board
@@ -344,7 +339,7 @@ public class DestroyAi extends SpellAbilityAi {
list.removeAll(preferred);
if (preferred.isEmpty() && !mandatory) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
while (sa.canAddMoreTarget()) {
@@ -352,16 +347,19 @@ public class DestroyAi extends SpellAbilityAi {
if (!sa.isMinTargetChosen()) {
if (!mandatory) {
sa.resetTargets();
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
} else {
break;
}
} else {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else {
Card c = ComputerUtilCard.getBestAI(preferred);
if (sa.canTarget(c)) {
sa.getTargets().add(c);
}
preferred.remove(c);
}
}
@@ -382,14 +380,25 @@ public class DestroyAi extends SpellAbilityAi {
} else {
c = ComputerUtilCard.getCheapestPermanentAI(list, sa, false);
}
if (sa.canTarget(c)) {
sa.getTargets().add(c);
}
list.remove(c);
}
}
return sa.isTargetNumberValid();
if (sa.isTargetNumberValid()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return mandatory;
sa.resetTargets();
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
} else {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}

View File

@@ -23,38 +23,23 @@ public class DestroyAllAi extends SpellAbilityAi {
* @see forge.card.abilityfactory.SpellAiLogic#doTriggerAINoCost(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility, boolean)
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return doMassRemovalLogic(ai, sa);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
return doMassRemovalLogic(aiPlayer, sa);
}
@Override
protected boolean canPlayAI(final Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, SpellAbility sa) {
// AI needs to be expanded, since this function can be pretty complex
// based on what the expected targets could be
final Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard();
if (abCost != null) {
// AI currently disabled for some costs
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
return false;
}
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if ("FellTheMighty".equals(aiLogic)) {
@@ -64,7 +49,7 @@ public class DestroyAllAi extends SpellAbilityAi {
return doMassRemovalLogic(ai, sa);
}
public static boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
public static AiAbilityDecision doMassRemovalLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
@@ -72,7 +57,7 @@ public class DestroyAllAi extends SpellAbilityAi {
final int CREATURE_EVAL_THRESHOLD = 200 / (!sa.usesTargeting() ? ai.getOpponents().size() : 1);
if (logic.equals("Always")) {
return true; // e.g. Tetzimoc, Primal Death, where we want to cast the permanent even if the removal trigger does nothing
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
String valid = sa.getParamOrDefault("ValidCards", "");
@@ -92,7 +77,7 @@ public class DestroyAllAi extends SpellAbilityAi {
opplist = CardLists.filter(opplist, predicate);
ailist = CardLists.filter(ailist, predicate);
if (opplist.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.usesTargeting()) {
@@ -101,7 +86,7 @@ public class DestroyAllAi extends SpellAbilityAi {
sa.getTargets().add(opponent);
ailist.clear();
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -110,30 +95,35 @@ public class DestroyAllAi extends SpellAbilityAi {
int numAiCanSave = Math.min(CardLists.count(ai.getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, ailist.size());
int numOppsCanSave = Math.min(CardLists.count(ai.getOpponents().getCreaturesInPlay(), CardPredicates.isColor(MagicColor.WHITE).and(CardPredicates.UNTAPPED)) * 2, opplist.size());
return numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave);
if (numOppsCanSave < opplist.size() && (ailist.size() - numAiCanSave < opplist.size() - numOppsCanSave)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (numAiCanSave < ailist.size() && (opplist.size() - numOppsCanSave < ailist.size() - numAiCanSave)) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// If effect is destroying creatures and AI is about to lose, activate effect anyway no matter what!
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, ai.getGame().getCombat()))) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// If effect is destroying creatures and AI is about to get low on life, activate effect anyway if difference in lost permanents not very much
if ((!CardLists.getType(opplist, "Creature").isEmpty()) && (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS))
&& (ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat()))
&& ((ComputerUtilCard.evaluatePermanentList(ailist) - 6) >= ComputerUtilCard.evaluatePermanentList(opplist))) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// if only creatures are affected evaluate both lists and pass only if human creatures are more valuable
if (CardLists.getNotType(opplist, "Creature").isEmpty() && CardLists.getNotType(ailist, "Creature").isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(ailist) + CREATURE_EVAL_THRESHOLD < ComputerUtilCard.evaluateCreatureList(opplist)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForMain2);
}
// test whether the human can kill the ai next turn
@@ -146,39 +136,42 @@ public class DestroyAllAi extends SpellAbilityAi {
}
}
if (!containsAttacker) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
AiBlockController block = new AiBlockController(ai, false);
block.assignBlockersForCombat(combat);
if (ComputerUtilCombat.lifeInSeriousDanger(ai, combat)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} // only lands involved
else if (CardLists.getNotType(opplist, "Land").isEmpty() && CardLists.getNotType(ailist, "Land").isEmpty()) {
if (ai.isCardInPlay("Crucible of Worlds") && !opponent.isCardInPlay("Crucible of Worlds")) {
return true;
// TODO Should care about any land recursion, not just Crucible of Worlds
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
// evaluate the situation with creatures on the battlefield separately, as that's where the AI typically makes mistakes
CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = opponent.getCreaturesInPlay();
if (!oppCreatures.isEmpty()) {
if (ComputerUtilCard.evaluateCreatureList(aiCreatures) < ComputerUtilCard.evaluateCreatureList(oppCreatures) + CREATURE_EVAL_THRESHOLD) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// check if the AI would lose more lands than the opponent would
if (ComputerUtilCard.evaluatePermanentList(ailist) > ComputerUtilCard.evaluatePermanentList(opplist) + 1) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} // otherwise evaluate both lists by CMC and pass only if human permanents are more valuable
else if ((ComputerUtilCard.evaluatePermanentList(ailist) + 3) >= ComputerUtilCard.evaluatePermanentList(opplist)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}

View File

@@ -20,26 +20,21 @@ import forge.util.TextUtil;
import java.util.Map;
public class DigAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Card host = sa.getHostCard();
Player libraryOwner = ai;
if (!willPayCosts(ai, sa, sa.getPayCosts(), host)) {
return false;
}
if (sa.usesTargeting()) {
sa.resetTargets();
if (!sa.canTarget(opp)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(opp);
libraryOwner = opp;
@@ -47,29 +42,21 @@ public class DigAi extends SpellAbilityAi {
// return false if nothing to dig into
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
return false;
}
if ("Never".equals(sa.getParam("AILogic"))) {
return false;
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
return false;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// don't deck yourself
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// Don't use draw abilities before main 2 if possible
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final String num = sa.getParam("DigNum");
@@ -87,14 +74,14 @@ public class DigAi extends SpellAbilityAi {
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()) - manaToSave;
if (numCards <= 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
root.setXManaCostPaid(numCards);
}
}
if (playReusable(ai, sa)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
@@ -102,24 +89,24 @@ public class DigAi extends SpellAbilityAi {
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
&& !ComputerUtil.activateForCost(sa, ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if ("MadSarkhanDigDmg".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.SarkhanTheMad.considerDig(ai, sa);
}
return !ComputerUtil.preventRunAwayActivations(sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player aiPlayer) {
// TODO: improve this check in ways that may be specific to a subability
return canPlayAI(aiPlayer, sa);
return canPlay(aiPlayer, sa);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final SpellAbility root = sa.getRootAbility();
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
@@ -137,12 +124,16 @@ public class DigAi extends SpellAbilityAi {
int manaToSave = Integer.parseInt(TextUtil.split(sa.getParam("AILogic"), '.')[1]);
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, true) - manaToSave;
if (numCards <= 0) {
return mandatory;
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(100, AiPlayDecision.CantPlayAi);
}
}
root.setXManaCostPaid(numCards);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
@@ -203,8 +194,6 @@ public class DigAi extends SpellAbilityAi {
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {
Card topc = player.getZone(ZoneType.Library).get(0);
// AI actions for individual cards (until this AI can be generalized)
if (sa.getHostCard() != null) {
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Explorer's Scope")) {
// for Explorer's Scope, always put a land on the battlefield tapped
// (TODO: might not always be a good idea, e.g. when a land ETBing can have detrimental effects)
@@ -212,7 +201,6 @@ public class DigAi extends SpellAbilityAi {
} else if ("AlwaysConfirm".equals(sa.getParam("AILogic"))) {
return true;
}
}
// looks like perfect code for Delver of Secrets, but what about other cards?
return topc.isInstant() || topc.isSorcery();

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
@@ -14,13 +12,12 @@ import forge.game.zone.ZoneType;
import java.util.Map;
public class DigMultipleAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Game game = ai.getGame();
Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
final Card host = sa.getHostCard();
@@ -29,7 +26,7 @@ public class DigMultipleAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(opp);
libraryOwner = opp;
@@ -37,33 +34,29 @@ public class DigMultipleAi extends SpellAbilityAi {
// return false if nothing to dig into
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if ("Never".equals(sa.getParam("AILogic"))) {
return false;
} else if ("AtOppEOT".equals(sa.getParam("AILogic"))) {
if (!(game.getPhaseHandler().getNextTurn() == ai && game.getPhaseHandler().is(PhaseType.END_OF_TURN))) {
return false;
}
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// don't deck yourself
if (sa.hasParam("DestinationZone2") && !"Library".equals(sa.getParam("DestinationZone2"))) {
int numToDig = AbilityUtils.calculateAmount(host, sa.getParam("DigNum"), sa);
if (libraryOwner == ai && ai.getCardsIn(ZoneType.Library).size() <= numToDig + 2) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// Don't use draw abilities before main 2 if possible
if (game.getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !sa.hasParam("DestinationZone") && !ComputerUtil.castSpellInMain1(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (playReusable(ai, sa)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if ((!game.getPhaseHandler().getNextTurn().equals(ai)
@@ -71,14 +64,14 @@ public class DigMultipleAi extends SpellAbilityAi {
&& !sa.hasParam("PlayerTurn") && !isSorcerySpeed(sa, ai)
&& (ai.getCardsIn(ZoneType.Hand).size() > 1 || game.getPhaseHandler().getPhase().isBefore(PhaseType.DRAW))
&& !ComputerUtil.activateForCost(sa, ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return !ComputerUtil.preventRunAwayActivations(sa);
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = AiAttackController.choosePreferredDefenderPlayer(ai);
if (sa.usesTargeting()) {
sa.resetTargets();
@@ -89,7 +82,7 @@ public class DigMultipleAi extends SpellAbilityAi {
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/* (non-Javadoc)

View File

@@ -1,8 +1,6 @@
package forge.ai.ability;
import forge.ai.AiAttackController;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
@@ -11,7 +9,6 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import java.util.List;
import java.util.Map;
@@ -19,7 +16,7 @@ import java.util.Map;
public class DigUntilAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
double chance = .4; // 40 percent chance with instant speed stuff
@@ -42,7 +39,7 @@ public class DigUntilAi extends SpellAbilityAi {
// material in the library after using it several times.
// TODO: maybe this should happen for any DigUntil SA with RevealedDestination$ Graveyard?
if (ai.getCardsIn(ZoneType.Library).size() < 20) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if ("Land.Basic".equals(sa.getParam("Valid"))
&& ai.getZone(ZoneType.Hand).contains(CardPredicates.LANDS_PRODUCING_MANA)) {
@@ -52,7 +49,7 @@ public class DigUntilAi extends SpellAbilityAi {
// This is important for Replenish/Living Death type decks
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
&& !ai.getGame().getPhaseHandler().isPlayerTurn(ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -60,7 +57,7 @@ public class DigUntilAi extends SpellAbilityAi {
if (sa.usesTargeting()) {
sa.resetTargets();
if (!sa.canTarget(opp)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(opp);
libraryOwner = opp;
@@ -68,7 +65,7 @@ public class DigUntilAi extends SpellAbilityAi {
if (sa.hasParam("Valid")) {
final String valid = sa.getParam("Valid");
if (CardLists.getValidCards(ai.getCardsIn(ZoneType.Library), valid, source.getController(), source, sa).isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -80,7 +77,7 @@ public class DigUntilAi extends SpellAbilityAi {
if (root.getXManaCostPaid() == null) {
int numCards = ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger());
if (numCards <= 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
root.setXManaCostPaid(numCards);
}
@@ -88,15 +85,14 @@ public class DigUntilAi extends SpellAbilityAi {
// return false if nothing to dig into
if (libraryOwner.getCardsIn(ZoneType.Library).isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
return randomReturn;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
sa.resetTargets();
if (sa.isCurse()) {
@@ -116,7 +112,7 @@ public class DigUntilAi extends SpellAbilityAi {
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/* (non-Javadoc)

View File

@@ -26,31 +26,29 @@ import forge.util.collect.FCollectionView;
public class DiscardAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Cost abCost = sa.getPayCosts();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
// temporarily disabled until better AI
if (!willPayCosts(ai, sa, abCost, source)) {
return false;
}
if ("Chandra, Flamecaller".equals(sourceName)) {
final int hand = ai.getCardsIn(ZoneType.Hand).size();
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
if (MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand))) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
if (aiLogic.equals("VolrathsShapeshifter")) {
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
}
final boolean humanHasHand = ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).size() > 0;
final boolean humanHasHand = !ai.getWeakestOpponent().getCardsIn(ZoneType.Hand).isEmpty();
if (sa.usesTargeting()) {
if (!discardTargetAI(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
// TODO: Add appropriate restrictions
@@ -64,7 +62,7 @@ public class DiscardAi extends SpellAbilityAi {
} else {
// defined to the human, so that's fine as long the human has cards
if (!humanHasHand) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
@@ -78,12 +76,12 @@ public class DiscardAi extends SpellAbilityAi {
final int cardsToDiscard = Math.min(ComputerUtilCost.getMaxXValue(sa, ai, sa.isTrigger()), ai.getWeakestOpponent()
.getCardsIn(ZoneType.Hand).size());
if (cardsToDiscard < 1) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.setXManaCostPaid(cardsToDiscard);
} else {
if (AbilityUtils.calculateAmount(source, sa.getParam("NumCards"), sa) < 1) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -113,7 +111,7 @@ public class DiscardAi extends SpellAbilityAi {
}
}
if (numDiscard == 0) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -121,27 +119,25 @@ public class DiscardAi extends SpellAbilityAi {
// Don't use discard abilities before main 2 if possible
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN2)
&& !sa.hasParam("ActivationPhases") && !aiLogic.startsWith("AnyPhase")) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (aiLogic.equals("AnyPhaseIfFavored")) {
if (ai.getGame().getCombat() != null) {
if (ai.getCardsIn(ZoneType.Hand).size() < ai.getGame().getCombat().getDefenderPlayerByAttacker(source).getCardsIn(ZoneType.Hand).size()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(0.9, sa.getActivationsThisTurn());
// some other variables here, like handsize vs. maxHandSize
return randomReturn;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
@@ -166,7 +162,7 @@ public class DiscardAi extends SpellAbilityAi {
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) {
PlayerCollection targetableOpps = ai.getOpponents().filter(PlayerPredicates.isTargetableBy(sa));
Player opp = targetableOpps.min(PlayerPredicates.compareByLife());
@@ -176,7 +172,7 @@ public class DiscardAi extends SpellAbilityAi {
} else if (mandatory && sa.canTarget(ai)) {
sa.getTargets().add(ai);
} else {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
@@ -184,7 +180,7 @@ public class DiscardAi extends SpellAbilityAi {
if ("AtLeast2".equals(sa.getParam("AILogic"))) {
final List<Player> players = AbilityUtils.getDefinedPlayers(sa.getHostCard(), sa.getParam("Defined"), sa);
if (players.isEmpty() || players.get(0).getCardsIn(ZoneType.Hand).size() < 2) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
}
@@ -196,18 +192,22 @@ public class DiscardAi extends SpellAbilityAi {
}
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
// Drawback AI improvements
// if parent draws cards, make sure cards in hand + cards drawn > 0
if (sa.usesTargeting()) {
return discardTargetAI(ai, sa);
if (discardTargetAI(ai, sa)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
// TODO: check for some extra things
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message, Map<String, Object> params) {

View File

@@ -1,9 +1,6 @@
package forge.ai.ability;
import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil;
import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
@@ -16,12 +13,8 @@ import java.util.Map;
public class DiscoverAi extends SpellAbilityAi {
@Override
protected boolean checkApiLogic(final Player ai, final SpellAbility sa) {
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite loop
}
return true;
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/**
@@ -36,8 +29,12 @@ public class DiscoverAi extends SpellAbilityAi {
* @return a boolean.
*/
@Override
protected boolean doTriggerAINoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
return mandatory || checkApiLogic(ai, sa);
protected AiAbilityDecision doTriggerNoCost(final Player ai, final SpellAbility sa, final boolean mandatory) {
if (mandatory) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return checkApiLogic(ai, sa);
}
@Override

View File

@@ -1,23 +1,23 @@
package forge.ai.ability;
import forge.ai.AiAbilityDecision;
import forge.ai.AiPlayDecision;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.MyRandom;
import java.util.List;
public class DrainManaAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
// AI cannot use this properly until he can use SAs during Humans turn
final Card source = sa.getHostCard();
final Player opp = ai.getWeakestOpponent();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
if (!sa.usesTargeting()) {
// assume we are looking to tap human's stuff
@@ -25,56 +25,58 @@ public class DrainManaAi extends SpellAbilityAi {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (!defined.contains(opp)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
sa.resetTargets();
sa.getTargets().add(opp);
}
return randomReturn;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getWeakestOpponent();
final Card source = sa.getHostCard();
if (!sa.usesTargeting()) {
if (mandatory) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
return defined.contains(opp);
if (defined.contains(opp)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else {
sa.resetTargets();
sa.getTargets().add(opp);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
// AI cannot use this properly until he can use SAs during Humans turn
final Card source = sa.getHostCard();
boolean randomReturn = true;
if (!sa.usesTargeting()) {
final List<Player> defined = AbilityUtils.getDefinedPlayers(source, sa.getParam("Defined"), sa);
if (defined.contains(ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
sa.resetTargets();
sa.getTargets().add(ai.getWeakestOpponent());
}
return randomReturn;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}

View File

@@ -26,7 +26,6 @@ import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CounterEnumType;
import forge.game.card.CounterType;
import forge.game.cost.*;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -42,43 +41,41 @@ public class DrawAi extends SpellAbilityAi {
* @see forge.ai.SpellAbilityAi#checkApiLogic(forge.game.player.Player, forge.game.spellability.SpellAbility)
*/
@Override
protected boolean checkApiLogic(Player ai, SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(Player ai, SpellAbility sa) {
if (!targetAI(ai, sa, false)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
if (sa.usesTargeting()) {
final Player player = sa.getTargets().getFirstTargetedPlayer();
if (player != null && player.isOpponentOf(ai)) {
return true;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
// prevent run-away activations - first time will always return true
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false;
}
if (ComputerUtil.playImmediately(ai, sa)) {
return true;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Don't tap creatures that may be able to block
if (ComputerUtil.waitForBlocking(sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.WaitForCombat);
}
if (!canLoot(ai, sa)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (ComputerUtilCost.isSacrificeSelfCost(sa.getPayCosts())) {
// Canopy lands and other cards that sacrifice themselves to draw cards
return ai.getCardsIn(ZoneType.Hand).isEmpty()
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5); // TODO: make this configurable in the AI profile
if (ai.getCardsIn(ZoneType.Hand).isEmpty()
|| (sa.getHostCard().isLand() && ai.getLandsInPlay().size() >= 5)) {
// TODO: make this configurable in the AI profile
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return true;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
/*
@@ -161,8 +158,6 @@ public class DrawAi extends SpellAbilityAi {
// LifeLessThan logic presupposes activation as soon as possible in an
// attempt to save the AI from dying
return true;
} else if (logic.equals("AtOppEOT")) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
} else if (logic.equals("RespondToOwnActivation")) {
return !ai.getGame().getStack().isEmpty() && ai.getGame().getStack().peekAbility().getHostCard().equals(sa.getHostCard());
} else if ((!ph.getNextTurn().equals(ai) || ph.getPhase().isBefore(PhaseType.END_OF_TURN))
@@ -175,8 +170,12 @@ public class DrawAi extends SpellAbilityAi {
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay());
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
if (targetAI(ai, sa, sa.isTrigger() && sa.getHostCard().isInPlay())) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
/**
@@ -370,7 +369,7 @@ public class DrawAi extends SpellAbilityAi {
// try to make opponent lose to poison
// currently only Caress of Phyrexia
if (getPoison != null && oppA.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (getPoison != null && oppA.canReceiveCounters(CounterEnumType.POISON)) {
if (oppA.getPoisonCounters() + numCards > 9) {
sa.getTargets().add(oppA);
return true;
@@ -414,7 +413,7 @@ public class DrawAi extends SpellAbilityAi {
}
}
if (getPoison != null && ai.canReceiveCounters(CounterType.get(CounterEnumType.POISON))) {
if (getPoison != null && ai.canReceiveCounters(CounterEnumType.POISON)) {
if (numCards + ai.getPoisonCounters() >= 8) {
aiTarget = false;
}
@@ -472,7 +471,7 @@ public class DrawAi extends SpellAbilityAi {
}
// ally would lose because of poison
if (getPoison != null && ally.canReceiveCounters(CounterType.get(CounterEnumType.POISON)) && ally.getPoisonCounters() + numCards > 9) {
if (getPoison != null && ally.canReceiveCounters(CounterEnumType.POISON) && ally.getPoisonCounters() + numCards > 9) {
continue;
}
@@ -515,12 +514,17 @@ public class DrawAi extends SpellAbilityAi {
return false;
}
if ((computerHandSize + numCards > computerMaxHandSize)
&& game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX) {
if ((computerHandSize + numCards > computerMaxHandSize)) {
// Don't draw too many cards and then risk discarding cards at EOT
if (!drawback) {
if (game.getPhaseHandler().isPlayerTurn(ai)
&& !sa.isTrigger()
&& !assumeSafeX
&& !drawback) {
return false;
}
if (computerHandSize > computerMaxHandSize) {
// Don't make my hand size get too big if already at max
return false;
}
}
@@ -529,12 +533,16 @@ public class DrawAi extends SpellAbilityAi {
} // drawTargetAI()
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (!mandatory && !willPayCosts(ai, sa, sa.getPayCosts(), sa.getHostCard())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return targetAI(ai, sa, mandatory);
if (targetAI(ai, sa, mandatory)) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.TargetingFailed);
}
}
/* (non-Javadoc)

View File

@@ -35,7 +35,7 @@ import java.util.Map;
public class EffectAi extends SpellAbilityAi {
@Override
protected boolean canPlayAI(final Player ai,final SpellAbility sa) {
protected AiAbilityDecision checkApiLogic(final Player ai, final SpellAbility sa) {
final Game game = ai.getGame();
boolean randomReturn = MyRandom.getRandom().nextFloat() <= .6667;
String logic = "";
@@ -45,12 +45,7 @@ public class EffectAi extends SpellAbilityAi {
final PhaseHandler phase = game.getPhaseHandler();
if (logic.equals("BeginningOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isAfter(PhaseType.DRAW)) {
return false;
}
randomReturn = true;
} else if (logic.equals("EndOfOppTurn")) {
if (!phase.getPlayerTurn().isOpponentOf(ai) || phase.getPhase().isBefore(PhaseType.END_OF_TURN)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
@@ -64,20 +59,20 @@ public class EffectAi extends SpellAbilityAi {
worthHolding = true;
}
if (!worthHolding) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
}
} else if (logic.equals("RestrictBlocking")) {
if (!phase.isPlayerTurn(ai) || phase.getPhase().isBefore(PhaseType.COMBAT_BEGIN)
|| phase.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (sa.getPayCosts().getTotalMana().countX() > 0 && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
// Set PayX here to half the remaining mana to allow for Main 2 and other combat shenanigans.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai, sa.isTrigger()) / 2;
if (xPay == 0) { return false; }
if (xPay == 0) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
sa.setXManaCostPaid(xPay);
}
@@ -90,23 +85,27 @@ public class EffectAi extends SpellAbilityAi {
int potentialDmg = 0;
List<Card> currentAttackers = new ArrayList<>();
if (possibleBlockers.isEmpty()) { return false; }
if (possibleBlockers.isEmpty()) { return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi); }
for (final Card creat : possibleAttackers) {
if (CombatUtil.canAttack(creat, opp) && possibleBlockers.size() > 1) {
potentialDmg += creat.getCurrentPower();
if (potentialDmg >= oppLife) { return true; }
if (potentialDmg >= oppLife) { return new AiAbilityDecision(100, AiPlayDecision.WillPlay); }
}
if (combat != null && combat.isAttacking(creat)) {
currentAttackers.add(creat);
}
}
return currentAttackers.size() > possibleBlockers.size();
if (currentAttackers.size() > possibleBlockers.size()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (logic.equals("Fog")) {
FogAi fogAi = new FogAi();
if (!fogAi.canPlayAI(ai, sa)) {
return false;
if (!fogAi.canPlay(ai, sa).willingToPlay()) {
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -124,14 +123,14 @@ public class EffectAi extends SpellAbilityAi {
}
if (!canTgt) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else {
List<Card> list = game.getCombat().getAttackers();
list = CardLists.getTargetableCards(list, sa);
Card target = ComputerUtilCard.getBestCreatureAI(list);
if (target == null) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
sa.getTargets().add(target);
}
@@ -139,7 +138,7 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true;
} else if (logic.equals("ChainVeil")) {
if (!phase.isPlayerTurn(ai) || !phase.getPhase().equals(PhaseType.MAIN2) || ai.getPlaneswalkersInPlay().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("WillCastCreature") && ai.isAI()) {
@@ -150,17 +149,17 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = true;
} else if (logic.equals("Main1")) {
if (phase.getPhase().isBefore(PhaseType.MAIN1)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("Main2")) {
if (phase.getPhase().isBefore(PhaseType.MAIN2)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
randomReturn = true;
} else if (logic.equals("Evasion")) {
if (!phase.isPlayerTurn(ai)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean shouldPlay = false;
@@ -185,10 +184,10 @@ public class EffectAi extends SpellAbilityAi {
break;
}
return shouldPlay;
return shouldPlay ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
if (game.getStack().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
boolean threatened = false;
for (final SpellAbilityStackInstance stackInst : game.getStack()) {
@@ -204,7 +203,7 @@ public class EffectAi extends SpellAbilityAi {
randomReturn = threatened;
} else if (logic.equals("Prevent")) { // prevent burn spell from opponent
if (game.getStack().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
final SpellAbility saTop = game.getStack().peekAbility();
final Card host = saTop.getHostCard();
@@ -215,10 +214,10 @@ public class EffectAi extends SpellAbilityAi {
final ApiType type = saTop.getApi();
if (type == ApiType.DealDamage || type == ApiType.DamageAll) { // burn spell
sa.getTargets().add(saTop);
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("NoGain")) {
// basic logic to cancel GainLife on stack
if (!game.getStack().isEmpty()) {
@@ -228,14 +227,14 @@ public class EffectAi extends SpellAbilityAi {
while (topStack != null) {
if (topStack.getApi() == ApiType.GainLife) {
if ("You".equals(topStack.getParam("Defined")) || topStack.isTargeting(activator) || (!topStack.usesTargeting() && !topStack.hasParam("Defined"))) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
} else if (topStack.getApi() == ApiType.DealDamage && topStack.getHostCard().hasKeyword(Keyword.LIFELINK)) {
Card host = topStack.getHostCard();
for (GameEntity target : topStack.getTargets().getTargetEntities()) {
if (ComputerUtilCombat.predictDamageTo(target,
AbilityUtils.calculateAmount(host, topStack.getParam("NumDmg"), topStack), host, false) > 0) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
@@ -249,11 +248,11 @@ public class EffectAi extends SpellAbilityAi {
final Player attackingPlayer = combat.getAttackingPlayer();
if (attackingPlayer.isOpponentOf(ai) && attackingPlayer.canGainLife()) {
if (ComputerUtilCombat.checkAttackerLifelinkDamage(combat) > 0) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("NonCastCreature")) {
// TODO: add support for more cases with more convoluted API setups
if (!game.getStack().isEmpty()) {
@@ -265,13 +264,13 @@ public class EffectAi extends SpellAbilityAi {
boolean reanimator = "true".equalsIgnoreCase(topStack.getSVar("IsReanimatorCard"));
if (changeZone && (toBattlefield || reanimator)) {
if ("Creature".equals(topStack.getParam("ChangeType")) || topStack.getParamOrDefault("Defined", "").contains("Creature"))
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("Fight")) {
return FightAi.canFightAi(ai, sa, 0, 0);
return FightAi.canFightAi(ai, sa, 0,0);
} else if (logic.equals("Pump")) {
sa.resetTargets();
List<Card> options = CardUtil.getValidCardsToTarget(sa);
@@ -281,55 +280,55 @@ public class EffectAi extends SpellAbilityAi {
}
if (!options.isEmpty() && phase.isPlayerTurn(ai) && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(options));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("Burn")) {
// for DamageDeal sub-abilities (eg. Wild Slash, Skullcrack)
SpellAbility burn = sa.getSubAbility();
return SpellApiToAi.Converter.get(burn).canPlayAIWithSubs(ai, burn);
return SpellApiToAi.Converter.get(burn).canPlayWithSubs(ai, burn).willingToPlay() ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("YawgmothsWill")) {
return SpecialCardAi.YawgmothsWill.consider(ai, sa);
return SpecialCardAi.YawgmothsWill.consider(ai, sa) ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.startsWith("NeedCreatures")) {
// TODO convert to AiCheckSVar
if (ai.getCreaturesInPlay().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (logic.contains(":")) {
String[] k = logic.split(":");
int i = Integer.parseInt(k[1]);
return ai.getCreaturesInPlay().size() >= i;
return ai.getCreaturesInPlay().size() >= i ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (logic.equals("ReplaySpell")) {
CardCollection list = CardLists.getValidCards(game.getCardsIn(ZoneType.Graveyard), sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false, false)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
} else if (logic.equals("PeaceTalks")) {
Player nextPlayer = game.getNextPlayerAfter(ai);
// If opponent doesn't have creatures, preventing attacks don't mean as much
if (nextPlayer.getCreaturesInPlay().isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Only cast Peace Talks after you attack just in case you have creatures
if (!phase.is(PhaseType.MAIN2)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// Create a pseudo combat and see if my life is in danger
return randomReturn;
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("Bribe")) {
Card host = sa.getHostCard();
Combat combat = game.getCombat();
if (combat != null && combat.isAttacking(host, ai) && !combat.isBlocked(host)
&& phase.is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& !AiCardMemory.isRememberedCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN)) {
AiCardMemory.rememberCard(ai, host, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN); // ideally needs once per combat or something
return true;
&& !host.getAbilityActivatedThisTurn().getActivators(sa).contains(ai)) {
// ideally needs once per combat or something
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else if (logic.equals("CantRegenerate")) {
if (sa.usesTargeting()) {
CardCollection list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
@@ -350,19 +349,19 @@ public class EffectAi extends SpellAbilityAi {
});
if (list.isEmpty()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
// TODO check Stack for Effects that would destroy the selected card?
sa.getTargets().add(ComputerUtilCard.getBestAI(list));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if (sa.getParent() != null) {
// sub ability should be okay
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
} else if ("Self".equals(sa.getParam("RememberObjects"))) {
// the ones affecting itself are Nimbus cards, were opponent can activate this effect
Card host = sa.getHostCard();
if (!host.canBeDestroyed()) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
Map<AbilityKey, Object> runParams = AbilityKey.mapFromAffected(sa.getHostCard());
@@ -370,18 +369,18 @@ public class EffectAi extends SpellAbilityAi {
List<ReplacementEffect> repDestroyList = game.getReplacementHandler().getReplacementList(ReplacementType.Destroy, runParams, ReplacementLayer.Other);
// no Destroy Replacement, or one non-Regeneration one like Totem-Armor
if (repDestroyList.isEmpty() || repDestroyList.stream().anyMatch(CardTraitPredicates.hasParam("Regeneration").negate())) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if (cantRegenerateCheckCombat(host) || cantRegenerateCheckStack(host)) {
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
} else { //no AILogic
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
if ("False".equals(sa.getParam("Stackable"))) {
@@ -390,7 +389,7 @@ public class EffectAi extends SpellAbilityAi {
name = sa.getHostCard().getName() + "'s Effect";
}
if (sa.getActivatingPlayer().isCardInCommand(name)) {
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
}
@@ -406,20 +405,20 @@ public class EffectAi extends SpellAbilityAi {
break;
}
}
return canTgt;
return canTgt ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
} else {
sa.getTargets().add(ai);
}
}
return randomReturn;
return randomReturn ? new AiAbilityDecision(100, AiPlayDecision.WillPlay) : new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
protected AiAbilityDecision doTriggerNoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
if (sa.hasParam("AILogic")) {
if (canPlayAI(aiPlayer, sa)) {
return true; // if false, fall through further to do the mandatory stuff
if (canPlay(aiPlayer, sa).willingToPlay()) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
@@ -431,7 +430,7 @@ public class EffectAi extends SpellAbilityAi {
if (!oppPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
if (mandatory) {
@@ -441,14 +440,14 @@ public class EffectAi extends SpellAbilityAi {
if (!aiPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
return true;
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
}
return false;
return new AiAbilityDecision(0, AiPlayDecision.CantPlayAi);
}
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
return super.doTriggerNoCost(aiPlayer, sa, mandatory);
}
protected boolean cantRegenerateCheckCombat(Card host) {

View File

@@ -17,9 +17,7 @@
*/
package forge.ai.ability;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.ai.*;
import forge.game.card.Card;
import forge.game.card.CardLists;
import forge.game.combat.CombatUtil;
@@ -45,19 +43,17 @@ public final class EncodeAi extends SpellAbilityAi {
* </p>
* @param sa
* a {@link forge.game.spellability.SpellAbility} object.
* @param af
* a {@link forge.game.ability.AbilityFactory} object.
*
* @return a boolean.
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
protected AiAbilityDecision canPlay(Player aiPlayer, SpellAbility sa) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return true;
public AiAbilityDecision chkDrawback(SpellAbility sa, Player ai) {
return new AiAbilityDecision(100, AiPlayDecision.WillPlay);
}
/*

Some files were not shown because too many files have changed in this diff Show More