Compare commits

..

1021 Commits

Author SHA1 Message Date
Blacksmith
f78f8006d6 [maven-release-plugin] prepare release forge-1.6.4 2017-09-29 16:13:56 +00:00
Blacksmith
46c79b4213 Update README.txt for release 2017-09-29 16:12:26 +00:00
Agetian
e4fb8c20ac - Added a missing reference to Nest of Scarabs. 2017-09-29 15:28:10 +00:00
Agetian
5a8762edb1 - Do not rotate the foil effect on the card if it has no picture (it's then drawn vertically). 2017-09-29 15:00:35 +00:00
Agetian
6285c76935 - Fixed Oracle text for Master of Waves. 2017-09-29 14:33:38 +00:00
Agetian
77942b632f - A better way to detect whether a player can look at the card for the purpose of identifying valid split rotation. 2017-09-29 11:33:19 +00:00
Agetian
29261c34d5 - Attempt to avoid spoiling that the face down card is a split card if it's face down and can't legally be seen by the player.
- Rotate Aftermath split cards correctly when they're in the graveyard.
2017-09-29 11:24:00 +00:00
Agetian
2152e5f731 - Do not try to rotate the foil for the non-current zoomed card since they're shown non-rotated and will break the foil effect. 2017-09-29 11:01:41 +00:00
Agetian
b659833f71 - Documenting changes in CHANGES.txt. 2017-09-29 10:57:26 +00:00
Agetian
f7d626d8c6 - Fixed split cards not being foiled correctly in all cases because the game assumed the foil effect to be stored on one of the halves instead of on the original card state.
- A more comprehensive fix for rotating split cards with foil in mobile Forge that does not break them in hand/graveyard/whatever. Also, turned it into an option since it doesn't look good in portrait mode, especially on smaller cellphones and in single card zoom mode.
2017-09-29 10:54:56 +00:00
Agetian
f1c8ace081 - Some updates for the PS_XLN* puzzles. 2017-09-29 08:30:27 +00:00
Agetian
655015d33b - Some updates for the PS_XLN* puzzles. 2017-09-29 08:29:22 +00:00
Agetian
ec24a92987 - Added a way to visually rotate split cards in card zoomer in Desktop Forge. 2017-09-29 08:20:51 +00:00
Agetian
04237c6118 - Updating draft rankings. 2017-09-29 07:49:18 +00:00
Agetian
86f13e05d2 - Formatting fix. 2017-09-29 05:34:51 +00:00
Agetian
9a6146e1ba - One more tweak. 2017-09-28 18:22:17 +00:00
Agetian
0a8ce34252 - A more generic implementation for the previous commit. 2017-09-28 18:21:48 +00:00
Agetian
18529f47e7 - Special case for Bone Dancer and ordering graveyards in "With Relevant Cards" mode. 2017-09-28 18:19:33 +00:00
Agetian
98215be0fc - A couple Raid description tweaks and fixes. 2017-09-28 18:10:38 +00:00
Agetian
f2698ef38d - etbCounter keyword unification in Sekki, Seasons' Guide 2017-09-28 17:36:31 +00:00
Agetian
215ee66a02 - Comment tweak. 2017-09-28 17:33:50 +00:00
Agetian
e055657d13 - Added a SpellApiToAi assignment for AF GameDrawn. 2017-09-28 17:31:47 +00:00
Agetian
ec7f47dbe7 - Added Celestial Convergence.
- Added new effect AF GameDrawn that creates an intentional draw situation by game effect.
- Divine Intervention now uses this effect.
2017-09-28 17:31:04 +00:00
Agetian
e3e7e4d26a - I guess Divine Intervention needs to be RemAIDeck, since the AI will slab it senselessly (if it's even possible for a senseless card like that) 2017-09-28 16:36:42 +00:00
Agetian
118d7d735a - Added Divine Intervention (another silly old card that no one is probably going to play with, but someone had to script it, I guess). 2017-09-28 16:35:22 +00:00
Agetian
d7a8534354 - Fixed Snapping Sailback. 2017-09-28 16:11:20 +00:00
Agetian
9fe86bf72a - Vampire Nocturnus Avatar: ensure that its static ability only starts working when the game actually begins. 2017-09-28 16:08:38 +00:00
Agetian
1d76f89428 - Added puzzles PS_XLN1 and PS_XLN2. 2017-09-28 15:54:40 +00:00
Agetian
fc26af5a89 - Fixed Gonti, Lord of Luxury generated effect description. 2017-09-28 15:43:58 +00:00
Agetian
7f85a8f2e1 - A shorter name for the option to allow ordering cards in graveyard in mobile Forge (due to visual space constraints). 2017-09-28 13:40:47 +00:00
Agetian
e3ff8029de - Preparing Forge for Android publish 1.6.3.003 [hotfix]. 2017-09-28 13:23:52 +00:00
Agetian
70f5bdd339 - A couple fixes for the Kamigawa quest world. 2017-09-28 06:36:18 +00:00
Agetian
2ecc36ab12 - AI should not be so reckless with triggered pumps that result in losing the card at end of turn (e.g. Hazoret's Favor) 2017-09-28 06:24:44 +00:00
Agetian
a4c14c6be1 - Some improvements to the Splice AI: do not reconsider the SA unless at least something was chosen for splice to save processing time; reset the targets on the main SA because it'll need to be retargeted anyway. 2017-09-28 04:41:38 +00:00
Agetian
6cbda005e8 - Fixed Vampire Nocturnus Avatar. 2017-09-28 04:19:29 +00:00
kevlahnota
8ea99648f0 Try to fix rotated split card with foil overlay 2017-09-28 01:52:09 +00:00
Agetian
58a5d9b0e1 - Attempting to fix Splice onto Arcane AI: when adding splice effects by the AI, actually reconsider the entire SA (with spliced subs) via the canPlay routine (and set targets while at it) before deciding whether to play it or not. Might not be optimal, but at least it seems to stop the AI from wasting splice cards and making them disappear from the game into the void. Improvements are welcome. 2017-09-27 18:53:25 +00:00
Agetian
b969f2eac6 - Fixed a NPE in DiscardEffect. 2017-09-27 17:02:23 +00:00
Agetian
5c295e9080 - Turned CheckCondition into a generic top-level AILogic, used it for both Repeating Barrage and Sasaya, Orochi Ascendant at the same time. 2017-09-27 15:02:36 +00:00
Agetian
64a6c3c5bb - Fixed the AI cheating with Repeated Barrage Raid ability. 2017-09-27 14:59:07 +00:00
Agetian
22cc4c635a - Minor fix in CHANGES.txt. 2017-09-27 14:55:03 +00:00
Agetian
b5b96f1155 - Fixed preference capitalization. 2017-09-27 14:41:36 +00:00
Agetian
06b887cd93 - A somewhat more fine-grained and less spoiler-y option to order graveyards, now with three states (Never / With Relevant Cards / Always). 2017-09-27 14:40:48 +00:00
Agetian
c6ef376d15 - Fixed generated description for Uba Mask. 2017-09-27 13:24:09 +00:00
Agetian
6ef249195e - Minor clarification in CHANGES.txt. 2017-09-27 08:09:32 +00:00
Agetian
f6e99cf748 - Preparing Forge for Android publish 1.6.3.002 [incremental/bug fixes]. 2017-09-27 08:05:13 +00:00
Agetian
6d39088777 - Added NeedsOrderedGraveyard to Alms and Death Spark. 2017-09-27 08:04:28 +00:00
Agetian
e5cb026608 - Bushido AI: attempt to avoid accounting for it twice when predicting P/T bonuses. 2017-09-27 07:39:18 +00:00
Agetian
ce9dbc2b5f - Restoring support for Extended for the time being, part 2 2017-09-27 06:36:53 +00:00
Agetian
bfef677e93 - Restored support for Extended format (removing it from blocks.txt breaks Quest Mode completely). 2017-09-27 06:35:42 +00:00
Agetian
0d18a1d19a - PlayerControllerAi: when playing with ordered graveyards and there's a Volrath's Shapeshifter in the game, try to place the best creature on top of the graveyard for the most value if Volrath's Shapeshifter hits the battlefield. 2017-09-27 06:20:25 +00:00
Agetian
91b3b7194d - Updated ISSUES.txt. 2017-09-27 04:26:42 +00:00
Agetian
306d515e7f - Documenting changes in CHANGES.txt. 2017-09-27 04:21:05 +00:00
Agetian
0a21e03eac - Added Bosium Strip (currently with an implementation similar to Kess, Dissident Mage, which doesn't interact correctly in corner cases where another card also lets you cast cards from graveyard but allows you not to exile them; need a better way to check "a card cast this way" (by checking that it was cast from an effect of a particular source card). Improvements in this area are welcome). 2017-09-27 04:20:49 +00:00
Agetian
e112704a63 - Fixed Nissa's Judgment. 2017-09-27 03:51:50 +00:00
Agetian
c0bbf107c6 - Fixed an occasionally broken orderMoveToZoneList (fixes Sensei's Divining Top). 2017-09-27 03:51:37 +00:00
Agetian
84a6876265 - Added an experimental option to allow ordering cards going to graveyard when playing with cards that care about graveyard order (Volrath's Shapeshifter and others). Disabled by default. 2017-09-26 19:40:44 +00:00
Agetian
add9ffe5d9 - Boros Charm: fixed an ability description. 2017-09-26 18:22:09 +00:00
Agetian
c71f0b7e39 - Deploy the Gatewatch: look at the cards even if there are no valid choices. 2017-09-26 15:11:26 +00:00
Agetian
e934071716 - Volrath's Shapeshifter: switched to a less aggressive update schedule (no update unless necessary), which fixes interaction with composite triggers that consist of several related parts (e.g. Undying, Persist).
- Volrath's Shapeshifter: QoL update: do not show the same text and discard ability twice if Volrath's Shapeshifter is attempting to copy the text of another Volrath's Shapeshifter that is on top of the graveyard.
2017-09-26 13:55:28 +00:00
Agetian
5693cddc3a - Some refactoring in AiBlockController related to random trades.
- Enabling random trades for favorable gang double and triple blocks.
2017-09-26 12:48:22 +00:00
Agetian
561d27be0a - Comment style fix. 2017-09-26 10:20:04 +00:00
Agetian
2e19a2a99c - Fixed Mindbreak Trap. 2017-09-26 10:15:52 +00:00
Agetian
4055e421bc - Minor formatting tweak. 2017-09-26 10:14:00 +00:00
Indigo Dragon
792255b676 Adding a Commander banned list a la Tiny Leaders (Literally a la. Involved many copying and pasting)
Adding a Commander Format, so that decks can now be checked in the original deck editor. Don't know how to implement highlander rules though, (Aside from the obviously stupid way of Restricting EVERY CARD EVER MADE).

Retiring the Extended Format. This one's dead Jim, dead Jim, dead Jim. It's as dead as Frontier. Maybe. Yes. If anyone complains they can just remove the \\s.
2017-09-26 08:46:36 +00:00
Hanmac
0ddb8d9644 ExploreEffect: fixed trigger and counter part 2017-09-26 05:22:00 +00:00
Agetian
5c29555ae7 - Fixed a typo. 2017-09-26 03:49:59 +00:00
Agetian
cf5e6bde9a - Documenting changes in CHANGES.txt. 2017-09-26 03:43:48 +00:00
Agetian
515ddbb28d - Removed one more portion of the leftover DamageDone|OnlyOnce code, I believe this is the last one. 2017-09-26 03:28:13 +00:00
Agetian
18720e3693 - Some additional NPE protection in applyPotentialAttackCloneTriggers. 2017-09-25 13:28:19 +00:00
Agetian
6b21664dff - Documenting changes in CHANGES.txt. 2017-09-25 13:17:40 +00:00
Agetian
ca92f90f6d - Integrating Personal Ratings patch by Seravy. 2017-09-25 13:14:23 +00:00
Agetian
8a1ab40f3c - AF Explore: apparently there's no need for a special SpellDescription on it when used as an Execute target for a trigger. 2017-09-25 13:06:56 +00:00
Agetian
f738822cce - A more appropriate solution for the manland animation AI problem. 2017-09-25 11:21:48 +00:00
Agetian
2ef900443b - Fizzle DamageDoneOnce for cards returning to battlefield from graveyard 2017-09-25 11:14:54 +00:00
Agetian
917c6b7c54 - Removed one more portion of the now-unused DamageDone|OnlyOnce code which would crash Forge. 2017-09-25 11:13:30 +00:00
Agetian
966db8af9f - Improved the animate manland AI such that it doesn't try to animate manlands that are already tapped. 2017-09-25 11:03:54 +00:00
Indigo Dragon
07b26f03a7 Fixed some broken Conquest Decks 2017-09-25 08:22:25 +00:00
Agetian
d685997040 - ExploreAi: honor the DoNotDiscardIfAble SVar. 2017-09-25 08:06:14 +00:00
Agetian
fce6807a3b - Simple AI support for Explore (feel free to expand). 2017-09-25 08:02:08 +00:00
Agetian
e91b428bd5 - Removed the now-unused Explore hack from AF Dig. 2017-09-25 07:31:54 +00:00
Agetian
6581466239 - Fixed trigger name in Brazen Buccaneers. 2017-09-25 07:29:36 +00:00
Agetian
380a5bbadd - Script update: AF Explore (better AI support coming soon). 2017-09-25 07:28:24 +00:00
Agetian
4da83ff5f8 - Unbanning Partner commanders in Planar Conquest. 2017-09-25 07:21:39 +00:00
Agetian
d5cf8848fa - Some fixes for AF Explore. 2017-09-25 07:18:37 +00:00
Agetian
4ea6f9dd6a - Scripts update: use DamageDealtOnce for cards that say "whenever X deals damage" 2017-09-25 07:10:21 +00:00
Agetian
d8027b002d - Script update: DealtCombatDamageOnce -> DamageDealtOnce|CombatDamage$True 2017-09-25 06:58:33 +00:00
Agetian
623cda83d5 - Script update: CombatDamageDoneOnce -> DamageDoneOnce|CombatDamage$True 2017-09-25 06:46:19 +00:00
Agetian
99b3e4493b - Documenting changes in CHANGES.txt. 2017-09-25 06:42:12 +00:00
Agetian
025a201a7d - Convert DamageDone|OnlyOnce to the new trigger DamageDoneOnce. Remove code for DamageDone|OnlyOnce (no longer needed). 2017-09-25 06:41:13 +00:00
Agetian
3243555181 - Partner commander UI support for mobile Forge (both Constructed and Planar Conquest). 2017-09-25 06:29:47 +00:00
Agetian
867eae442b - Fix compile. 2017-09-25 05:40:49 +00:00
Hanmac
e8e80a7ac8 replace lifelink with generic damage trigger, and do lifelink there 2017-09-25 05:21:06 +00:00
Sol
480c88a73e Tempest Caller only targets opponents 2017-09-24 22:38:45 +00:00
Agetian
6f34a42034 - A somewhat less confusing Boneyard Parley AI. 2017-09-24 15:01:58 +00:00
Agetian
312421ed28 - ReplaceProduceMana: check the root ability for tap cost (in case the mana ability is a subability), fixes interaction of mana replacement effects (e.g. Mana Reflection) with cards that tap in the root ability (e.g. Nykthos, Shrine to Nyx). 2017-09-24 14:57:28 +00:00
Indigo Dragon
93e81fdc01 Gave a custom Energy icon to Journeyman Skin 2017-09-24 14:27:23 +00:00
Agetian
2d7f0f907b - Added StackDescription to Grim Captain's Call. 2017-09-24 14:13:54 +00:00
Agetian
6aca67efa6 - Made Boneyard Parley AI playable. 2017-09-24 14:07:06 +00:00
Indigo Dragon
bab2b9f528 Changed "CARDNAME can block an additional creature." to CARDNAME can block an additional creature each combat."
Notes: Possible weird interactions when giving a multiblock creature an additional block eg. Equipping a Night Market Guard with echo circlet. Further study required.
2017-09-24 12:33:45 +00:00
Indigo Dragon
07886140fb Several Ixalan changes, as well as adding Scry reminder texts to those that need them.
Notes: Grim Captain's Call is... weird. Needs fixing for the stack panel when cast (Largely because it doesn't target the creatures on cast, only returns them on resolution)

Also: Boneyard Parley works when the Human uses it, but AI absolutely butches the execution I can't explain. Added SVar:RemAIDeck:True just in case. Pls fix.
2017-09-24 11:55:39 +00:00
Agetian
90341ee27a - Some more ID cleanup in Ravnica. 2017-09-24 04:50:54 +00:00
Agetian
f78da4f637 - Some improvements to DoNotDiscardIfAble discard AI for corner cases, to avoid (very rare) situations where the AI would not discard anything or crash. 2017-09-24 04:48:07 +00:00
Agetian
d3b8ffe328 - Fixed Heartless Pillage. 2017-09-24 04:47:11 +00:00
Agetian
e0b25527b3 - Fixed a deck ID in the Ravnica quest world. 2017-09-24 04:32:22 +00:00
Agetian
56798a76c9 - Fixed generated text for Volrath's Shapeshifter. 2017-09-23 16:07:28 +00:00
Agetian
2faab75bd8 - Changed comment type. 2017-09-23 15:44:08 +00:00
Agetian
d4d7c5b35e - Removed the issue note for As Foretold from ISSUES.txt, it's an implementation issue that does not have a functional side effect for the end-user. There's a TODO entry in the relevant part of the code. 2017-09-23 15:43:18 +00:00
Agetian
bf07df8f7b - Fixed Oracle text of Terror of Kruin Pass. 2017-09-23 15:40:00 +00:00
Agetian
3622103a90 - Cavern of Souls AI: do not try to pay with the SA the card for which was already tapped, for this cost or for something else. 2017-09-23 14:12:48 +00:00
Agetian
31680b3849 - Preparing Forge for Android publish 1.6.3.001 [incremental/new release]. 2017-09-23 13:52:31 +00:00
Blacksmith
aa7611fceb Clear out release files in preparation for next release 2017-09-23 13:51:19 +00:00
Blacksmith
9f0a34455b [maven-release-plugin] prepare for next development iteration 2017-09-23 13:45:20 +00:00
Blacksmith
da51f8af37 [maven-release-plugin] prepare release forge-1.6.3 2017-09-23 13:45:13 +00:00
Blacksmith
170853f2cc Update README.txt for release 2017-09-23 13:43:19 +00:00
Agetian
df4b625ac4 - Adapted the DigEffect implementation of Explore until the effect is fully converted and it can be removed. 2017-09-23 11:58:47 +00:00
Hanmac
2d6ff3b74c basic Explore Effect 2017-09-23 11:47:57 +00:00
Agetian
a5b3b61052 - Fixed Scry AI scrying away basic lands thinking that they do not produce mana since they do not use a mana-producing SA as such and rely on the basic land type instead. 2017-09-23 11:16:58 +00:00
Agetian
1b1a56e77c - Improved support for Illusions-Donate, added deck The Great and Powerful Trixie 2, changed the deck The Great and Powerful Trixie 3 to be a more standard Legacy-legal Trix. 2017-09-23 09:04:16 +00:00
Agetian
e18dd07491 - Fixed Axis of Mortality. 2017-09-23 05:21:27 +00:00
Agetian
f94771730e - AI: Improved logic for Capsize. 2017-09-23 05:09:16 +00:00
Sol
cb24df8890 Fix Bloodcrazed text 2017-09-23 03:14:27 +00:00
Sol
499d72d5d6 - Fix SpellDescription for Deadeye Tracker 2017-09-23 03:10:42 +00:00
Sol
1bab2617b7 W16 isn't in Standard 2017-09-22 23:47:57 +00:00
Agetian
b5ed2daa81 - Some changes for the experimental Intuition logic as support for Illusions-Donate. 2017-09-22 19:36:48 +00:00
Agetian
c32cd456b6 - Improved AI logic for Firecannon Blast. 2017-09-22 14:55:17 +00:00
Agetian
0f9a71f07d - Sword of X and Y artifact cycle: added MustBeBlocked:AttackingPlayer since otherwise the AI generally ignores these powerful effects, typically losing to them or to the card advantage that they generate in a matter of few turns. 2017-09-22 09:06:48 +00:00
Agetian
a1711daead - Some improvements in ComputerUtilCombat regarding predicting attack clone effects and MustBeBlocked. 2017-09-22 09:04:47 +00:00
Indigo Dragon
5b6031f41d Policy: If a card was printed without a reminder text in a booster expansion or preconstructed deck (eg Commander), it no longer requires reminder text. IMA removes basic reminder texts for serval cards, like the mythic rares archangel_of_thune.txt and thundermaw_hellkite.txt 2017-09-22 08:29:03 +00:00
Agetian
c3787ab02f - A little improvement to the previous commit. 2017-09-22 05:55:17 +00:00
Agetian
9d1a216a20 - AI: Predict clone on attack effects like Tilonalli's Skinshifter when deciding which creatures to leave for blocking. 2017-09-22 05:53:49 +00:00
Hanmac
2603d08aa4 add more Damage Once Triggers 2017-09-22 05:36:33 +00:00
Agetian
990c0afee2 - Updated the Protection effects to use the timestamp-based changed keywords mechanism, this fixes interactions like Lignify + Reverent Mantra choosing Protection from green. 2017-09-22 05:00:54 +00:00
Sol
1bfd401ed7 Fix description for Verdant Sun's Avatar 2017-09-22 01:11:22 +00:00
Sol
22414bab7c Remove Defender from Lightning Rig Crew 2017-09-22 00:31:39 +00:00
Agetian
08222b0d5c - Moved the code fragment a bit. 2017-09-21 18:17:36 +00:00
Agetian
d599d29514 - ReplaceDamage: according to 119.8, if something would deal 0 damage, it deals no damage at all, and thus there is no event to replace (please double check). 2017-09-21 18:08:33 +00:00
Agetian
e5120c7074 - Updated RankingScraper.py to use the draftsim ranking scraper instead of the defunct bestiaire ranking scraper. 2017-09-21 17:54:51 +00:00
kevlahnota
68f36bf172 Rotate split cards when zoomed-in, text detail for effect and emblem instead of token 2017-09-21 14:29:08 +00:00
Agetian
ee025f9a13 - Asset file size correction. 2017-09-21 11:58:17 +00:00
Agetian
68891d18f4 - Some improvement to the experimental and currently disabled "hold land drops" AI logic. 2017-09-21 04:38:54 +00:00
Agetian
be4b7e7232 - A somewhat better implementation of "show another card in prompt" code that potentially allows extension later on for things that may need it. 2017-09-21 03:50:42 +00:00
Agetian
c3e03c17e8 - Deadeye Tormentor: Raid description format unification (em-dash style). 2017-09-21 03:38:05 +00:00
Sol
1dc63294b4 Missing Raid in description of Deadeye Tormentor 2017-09-21 03:12:50 +00:00
Sol
63191515d3 Catch much wider exceptions in buildCardGenDeck based on key card 2017-09-21 01:28:48 +00:00
Agetian
f8b072f14a - Added a (hacky) way to display the remembered card for a SA in the prompt panel, currently used by the Explore workaround implementation, but may be used for more (in cases where the wrong thing is displayed on mouse-over in complex SAs). 2017-09-20 19:37:59 +00:00
Agetian
d09862522b - Fixed capitalization in Iconic Masters definition file. 2017-09-20 17:54:59 +00:00
Agetian
ce5240223b - Updated quest opponent deck Guybrush Threepwood 2. 2017-09-20 17:46:49 +00:00
Agetian
5b8d2cb36a - AI: For Vraska, the Relic Seeker, play a little more conservatively to avoid losing the planeswalker by activating its -3 ability two times in a row 2017-09-20 17:43:52 +00:00
Agetian
d9b35ca1ee - Added XLN planeswalker decks to quest precons. 2017-09-20 15:24:21 +00:00
Agetian
e8433a1d80 - Fixed AI params for Makeshift Munitions. 2017-09-20 11:52:43 +00:00
Agetian
092aac82ea - SetStateAi: allow AILogic$ Always on SetState transform triggers (set this on Vance's Blasting Cannons). 2017-09-20 11:51:16 +00:00
Agetian
9039178927 - AI: Look in all graveyards for potential targets of Deathgorge Scavenger, starting with the opponents' graveyards and then ending with the AI's own. 2017-09-20 09:51:27 +00:00
Agetian
a8b2a627f8 - Added an AI deck hint to Deadeye Quartermaster. 2017-09-20 09:34:14 +00:00
Agetian
da8b85decd - AI: try to proc Raid in case an attack is likely instead of casting the creature early in main 1 2017-09-20 09:19:04 +00:00
Agetian
73786bfd94 - Do not use the old AE ligature conversion in column names of the item manager. 2017-09-20 05:36:30 +00:00
Agetian
e06ea82f62 - Some improvements to AnimateAi.
- Fixed Myth Realized ability text. Also, it shouldn't be a characteristic-defining ability since it's an ability granted to itself (point 4 in rule 604.3a), otherwise it doesn't interact correctly, for example, with Starfield of Nyx in presence of five enchantments.
2017-09-19 17:37:57 +00:00
Agetian
085732868c - Minor improvement to the experimental "hold land drops" AI option (currently disabled by default). 2017-09-19 16:42:59 +00:00
Agetian
3f7c512c63 - Improved AI cast params for Fleetwheel Cruiser. 2017-09-19 16:42:26 +00:00
Agetian
28d8b49de7 - Minot tweak to previous commit. 2017-09-19 15:16:27 +00:00
Agetian
c79d04f29a - Added an experimental logic for Intuition (currently disabled by default, pending testing and fine-tuning). 2017-09-19 15:15:58 +00:00
Agetian
d1ba022ce9 - Tweaked the deck Morkus Rex 3 a bit. 2017-09-19 12:37:07 +00:00
Agetian
d36fb16b12 - Migrating XLN cards from upcoming to the real folders. 2017-09-19 12:04:06 +00:00
Agetian
26e94d3ba6 - Minor tweak to quest opponent descriptions. 2017-09-19 03:24:45 +00:00
Agetian
d2cd5e7202 - ScryAi: in the general case, avoid activating before the end of opponent's turn before own turn if it requires tapping the source (especially if it's on a creature) and has a mana cost. 2017-09-19 03:22:45 +00:00
Agetian
0bfd5cc292 - Added quest opponents Guybrush Threepwood 1, Guybrush Threepwood 2, Morkus Rex 2, Morkus Rex 3 with Ixalan cards (Pirate and Dino themed, respectively). 2017-09-18 19:05:02 +00:00
Agetian
deb25f8299 - Added a reference to Gishath, Sun's Avatar. 2017-09-18 18:51:22 +00:00
Agetian
aab28fd3fd - [XLN] Fixed ability description for Captain Lannery Storm. 2017-09-18 17:03:41 +00:00
Agetian
d9759305a3 - Do not calculate damage to planeswalkers for the purpose of chump saving in case the AI profile specifies to always consider them threatened if something is attacking them. 2017-09-18 15:49:08 +00:00
Agetian
e015e57dda - Try to detect cases where the actual damage to planeswalkers will be zero after prevention. 2017-09-18 15:29:14 +00:00
Agetian
44a2f1880d - Some improvements to the "protect planeswalkers with weak chumps" logic. Enabling it in a conservative version by default.
- Several additional experimental toggles for ChangeZoneAllAi, will be used to fine-tune "bounce all" type spells in generic cases.
2017-09-18 15:26:15 +00:00
Agetian
2e3617ad1c - Storm Seeker: added a missing SVar reference. 2017-09-18 12:51:51 +00:00
Agetian
ae5e7d1e60 - One more tweak to the previous commit. 2017-09-18 12:47:56 +00:00
Agetian
a31c3dc611 - Readded a part of the reanimator-specific Survival of the Fittest code. 2017-09-18 12:47:17 +00:00
Agetian
2ee3ec303d - Fixed Survival of the Fittest AI, which was accidentally made to grab discard targets from the library, lol 2017-09-18 12:46:11 +00:00
Agetian
3d8a85a145 - A tweak to Skyshroud War Beast NeedsToPlay (may still not work reliably in multiplayer, depending on who the AI chooses; ideally could use its own AI logic or something). 2017-09-18 12:20:54 +00:00
Agetian
fc18153f02 - A tweak to DigUntilAi DontMillSelf. 2017-09-18 12:19:02 +00:00
Agetian
77c1875b6b - [XLN] Fixed River Herald's Boon mana cost. 2017-09-18 09:29:45 +00:00
Agetian
4388a84787 - Gaze of Adamaro: marked as AI-playable. 2017-09-18 06:30:32 +00:00
Agetian
0d3a9c8b16 - Some logic for cards like Sudden Impact that deal damage to the player by the number of cards in his hand. 2017-09-18 06:19:50 +00:00
Hanmac
2ca05b5634 add TriggerDamageDealOnce 2017-09-18 05:28:02 +00:00
Agetian
4569bca39d - ChangeZoneAllAi: for things that return creatures from the battlefield to wherever en masse (e.g. Evacuation), cast is as a last resort if the opponent is about to deal lethal damage and win.
- Skyshroud War Beast: added NoZeroToughnessAI param.
2017-09-18 04:35:14 +00:00
Agetian
c401a609a4 - Survival of the Fittest, Treasure Trove: added an AI cast preference parameter to avoid having a duplicate on the battlefield.
- Expanded AI cast preferences to potentially support cards the duplicate for which can be cast in case the opponent debuffed the original (currently not used by any cards, looking for good candidates).
2017-09-18 04:15:41 +00:00
Agetian
07a71f1059 - Survival of the Fittest AI: avoid mana lock by casting it at the end of opponent's turn. 2017-09-18 03:40:03 +00:00
Agetian
82f6f7d4d5 - [XLN] Assorted fixes. 2017-09-18 03:32:26 +00:00
Agetian
97f68a8247 - Removed a hard limit on playing Survival on the Fittest (needs a better solution, currently too restrictive and the AI will not cast another SotF even if the first one becomes locked in some way on board, e.g. Imprisoned on the Moon). 2017-09-17 20:06:21 +00:00
Agetian
1d9f867c75 - Added an implementation comment. 2017-09-17 19:50:53 +00:00
Agetian
ebd3c33051 - Added some simple SVar-based prediction of Reanimator decks, currently used by the Survival of the Fittest AI code.
- Added a worlds.txt entry for Kamigawa quest world.
2017-09-17 19:50:03 +00:00
Agetian
00cec4faa0 - Documenting changes in CHANGES.txt. 2017-09-17 18:42:25 +00:00
Agetian
690fa7bf78 - Added Kamigawa quest world by daitokujibiko. 2017-09-17 18:40:19 +00:00
Agetian
ace6474157 - Survival of the Fittest: no point in casting the second copy while the first one is active and in play. 2017-09-17 18:33:03 +00:00
Agetian
974a017044 - [Request] Improved AI logic for Spike Weaver. 2017-09-17 18:23:34 +00:00
Indigo Dragon
89917d4f75 Added CostDesc$ {5}{R}{R}{R} to mizzixs_mastery.txt
Removed some double spaces "  " for the rest.
2017-09-17 16:05:18 +00:00
Agetian
f651364e00 - [XLN] A couple fixes. 2017-09-17 12:35:52 +00:00
Agetian
2a8b43b7a3 - Comment fix. 2017-09-17 10:46:01 +00:00
Agetian
aff991690d - Improved DigUntil logic for Hermit Druid. 2017-09-17 10:43:55 +00:00
Agetian
7f095947ab - ShouldPumpCard: when testing for Infect, ensure that the opponent can actually receive poison counters. 2017-09-17 08:29:43 +00:00
Agetian
b18b2cff44 - PumpAi (Aristocrats logic): only check Infect in case the target can receive Poison counters. 2017-09-17 08:23:14 +00:00
Agetian
befd16238b - Arcane Adaptation: should not affect Sideboard. 2017-09-17 08:16:11 +00:00
Agetian
20d3e540b0 - One more tweak to the previous commit. 2017-09-17 08:09:37 +00:00
Agetian
cede2927c5 - Made the new experimental AI option chance-based (currently disabled by default). 2017-09-17 08:07:56 +00:00
Agetian
543aae0893 - Some tweaks and improvements in the experimental AI code. 2017-09-17 08:02:43 +00:00
Agetian
e9c55345b8 - Fixed Heartless Pillage.
- Fixed some non-UTF-8 symbols in scripts.
2017-09-17 07:45:21 +00:00
Agetian
8c7dc08844 - Added an experimental option to hold unused land drops until main 2 (disabled by default). 2017-09-17 06:22:30 +00:00
Agetian
3ae20885c4 - Removed and defaulted the experimental AI option to avoid counting static attack bonus effects in declare blockers twice. 2017-09-17 04:47:27 +00:00
Agetian
09042f1dbf - Minor code reorganization in ComputerUtilCard. 2017-09-17 04:44:07 +00:00
Agetian
6b5c52d2c8 - Fixed the AI ignoring the non-stacking KW list and repeatedly pumping cards with non-stacking keywords without any other benefit when attacking for lethal. 2017-09-17 04:17:28 +00:00
Agetian
e02e2c462b - Added Menace to non-stacking keywords list (fixes multiple AI activations). 2017-09-17 04:07:50 +00:00
Agetian
30a1ea7a4f - [XLN] Some fixes. 2017-09-17 03:56:45 +00:00
austinio7116
51a2383574 Fixed fathom fleet cutthroat typo 2017-09-16 20:39:18 +00:00
Agetian
7fbbb9b030 - A somewhat better variable name. 2017-09-16 16:24:10 +00:00
Agetian
f2d88796ed - A somewhat better timing for Thundering Wurm for the AI. 2017-09-16 15:55:34 +00:00
Agetian
1659ca88ca - Added AI logic for Mox Diamond. 2017-09-16 15:48:16 +00:00
Agetian
fd7ba65203 - Improved the AI detecting whether the attacking creature would be destroyed by blockers when it has first strike or double strike (fixes e.g. a useless power pump vs. a first striker). 2017-09-16 15:29:42 +00:00
Agetian
ae66502fd5 - Fixed the em-dashes in CardFactoryUtil code. 2017-09-16 15:28:55 +00:00
Agetian
cc06fb6b40 - Fixed a failing test. 2017-09-16 14:53:17 +00:00
Agetian
a60c6af30e - [XLN] Fixed Gishath, Sun's Avatar and Ranging Raptors. 2017-09-16 14:32:43 +00:00
Agetian
91e7cd6576 - [XLN] A couple fixes. 2017-09-16 11:46:51 +00:00
Agetian
d717a68445 - Documenting changes in CHANGES.txt. 2017-09-16 11:40:24 +00:00
Agetian
7f4dcf54a7 - AiBlockController: shifted priority for non-lethal gang blocks below necessary chump blockers to avoid cheating or dying accidentally to something. 2017-09-16 11:35:46 +00:00
Agetian
613238e0f9 - Fixed the interaction of Tribute with effects like Solemnity. 2017-09-16 11:04:53 +00:00
Agetian
72f7189aeb - Added a comment. 2017-09-16 10:46:06 +00:00
Agetian
ea2616434c - No need to choose the best possible creature for a non-lethal Menace block. 2017-09-16 10:45:19 +00:00
Agetian
0261f25996 - AiBlockController: added a routine to try to block a Menace creature with two creatures that don't kill it but neither of which get killed as well. 2017-09-16 10:41:37 +00:00
Agetian
f5db79ce69 - Added a patch to the Starter world by Seravy. 2017-09-16 10:25:06 +00:00
Agetian
236a7c91d5 - Comment fix. 2017-09-16 10:18:05 +00:00
Agetian
c6bae2116a - Added an extra mode to DamageDone trigger (OnlyOnce$ True) that tries to count the damage only once. Currently will do it in combat, but will not yet do it for noncombat simultaneous damage like Aura Barbs (feel free to improve).
- Effectively this replaces the double trigger setup for Enrage and other cards that require such a count (e.g. Fungusaur and friends).
2017-09-16 10:17:14 +00:00
Indigo Dragon
05d42d9518 Replace many " - " with "—" 2017-09-16 06:56:28 +00:00
austinio7116
19a6236fa9 Readding card based deck gen .dat files post rotation 2017-09-16 06:21:15 +00:00
austinio7116
115dbc8b12 Trying to update card based deck generation dat files 2017-09-16 06:17:23 +00:00
Agetian
03084c3ce3 - Moving IMA rankings before XLN rankings in order to maintain the historical order of sets that the file is arranged in. 2017-09-16 05:48:52 +00:00
Agetian
0260afa068 - Changed underscores to spaces in Ixalan draft rankings (hopefully it wasn't intentional that way?...) 2017-09-16 05:47:15 +00:00
Agetian
2f9044f53f - Added a missing reference. 2017-09-16 05:18:12 +00:00
Agetian
f195e85fb8 - Improved the cleanup for Siren's Ruse. 2017-09-16 05:17:57 +00:00
Agetian
65065343f9 - [XLN] Added Siren's Ruse. 2017-09-16 05:14:46 +00:00
Agetian
50c8014977 - Hostage Taker: added Artifact.Other to ChangeZone targeting constraints. 2017-09-16 04:21:18 +00:00
Agetian
911d0a4fae - Vance's Blasting Cannons: the trigger is optional 2017-09-16 04:18:40 +00:00
Agetian
d1bb81e404 - Arcane Adaptation: set it to affect Sideboard too for situations with cards you own outside the game (http://magicjudge.tumblr.com/post/165378548299/the-locust-god-official) 2017-09-16 04:17:06 +00:00
Sol
76526a0b2d No need for duplicating creature types 2017-09-16 03:19:33 +00:00
Sol
a9020d92ca Updated XLN for Blocks and Formats 2017-09-16 01:35:14 +00:00
Sol
74be6e6c14 Adding remaining XLN cards from Marek (P-Z) 2017-09-16 01:33:44 +00:00
Sol
f203168542 Adding remaining XLN cards from Marek (A-O) 2017-09-16 01:33:01 +00:00
Sol
3c03f5fd71 Adding Trilobite type 2017-09-16 01:30:17 +00:00
Sol
8cfc9d7793 Fixing Ixalan editions file 2017-09-16 00:17:48 +00:00
kevlahnota
ec5c500b8f Fix lag I introduced. Just learned that we should never create an instance of a new object inside the render call, because it will create a new instance every draw call. 2017-09-15 20:53:46 +00:00
austinio7116
8f7cbeb4ab First draftsim rankings added for XLN 2017-09-15 20:32:46 +00:00
Agetian
fb32273477 - Corrected a token name. 2017-09-15 16:42:45 +00:00
Agetian
1be49e8d8a - ComputerUtilCombat: attempt to prevent the combat AI from counting the Flanking debuff twice when predicting the power bonus of blocker. 2017-09-15 16:39:20 +00:00
Agetian
b7edc01952 - Working out some TODO items in Ixalan card scripts (treasure token images, previously unconfirmed P/T). 2017-09-15 16:12:37 +00:00
Agetian
0470878ddf - KLD+ Fat packs should be named Bundle. 2017-09-15 16:08:21 +00:00
Agetian
d07a65d553 - Bundle and Booster Box info for Ixalan. 2017-09-15 16:00:18 +00:00
Agetian
58218f10da - Added booster generation info to Ixalan.txt. 2017-09-15 15:57:59 +00:00
Agetian
690eebb6a1 - Added Trilobite creature type. 2017-09-15 15:52:58 +00:00
Agetian
9bd9936781 - Fixed capitalization in Ixalan.txt. 2017-09-15 15:40:01 +00:00
Indigo Dragon
bf62325329 Added Ixalan.txt Edition File 2017-09-15 15:38:08 +00:00
Agetian
a3775e0b57 - Fixed Deathgorge Scavenger and improved AI logic for it. 2017-09-15 15:38:02 +00:00
Agetian
03c0794b48 - Some improvements to the Aristocrats and Electrostatic Pummeler AI logic. 2017-09-15 15:08:31 +00:00
Agetian
b5edcaf5ac - Added a missing reference to Liliana, Defiant Necromancer. 2017-09-15 14:54:37 +00:00
kevlahnota
3723a4e656 Update fastReplace (return null if source string is null) 2017-09-15 14:46:29 +00:00
Agetian
b332038722 - Fixed the AI trying to flip Sasaya, Orochi Ascendant endlessly without satisfying its condition.
- Several script fixes from Marek.
2017-09-15 14:08:27 +00:00
Agetian
33df8dd5eb - PermanentAi: commented out the check of Planeswalker uniqueness by subtype (no longer relevant past Ixalan). 2017-09-15 09:48:29 +00:00
kevlahnota
7e982b327b Refactor some String.replace to use TextUtil.fastReplace 2017-09-15 09:01:58 +00:00
Agetian
91585464bd - [XLN] Added 4 cards by Marek. 2017-09-15 06:35:52 +00:00
Agetian
3c3f494034 - Simultaneous combat damage pass 2: "X is dealt combat damage" card update 2017-09-15 06:34:44 +00:00
Agetian
87cbc6229a - Fixed Soul Link. 2017-09-15 05:59:45 +00:00
Agetian
bd10364d27 - Simultaneous combat damage pass 1: "deals combat damage to X" card update 2017-09-15 05:52:08 +00:00
Agetian
36721dde56 - A tweak to the previous commit. 2017-09-15 05:14:05 +00:00
Agetian
6a74bd841a - Improved TriggerCombatDamageDoneOnce to propagate the amount of damage dealt to targets.
- Corrected Armadillo Cloak and Fungusaur as implementation examples for simultaneous combat damage (simultaneous noncombat damage like Aura Barbs is still impossible as of yet, feel free to improve if you know how).
2017-09-15 05:12:55 +00:00
Agetian
093b5451c3 - Some XLN fixes. 2017-09-15 04:12:13 +00:00
Agetian
896dff3fdc - [XLN] Added 15/09 cards by Marek. 2017-09-15 04:09:12 +00:00
kevlahnota
e8e23603c3 Rotate Plane/Phenomenon on FOptionPane 2017-09-14 22:47:56 +00:00
Agetian
426fe0fe40 - Scavenging Ooze doesn't need remember/cleanup to operate.
- Animation Module: set AILogic to AlwaysAtOppEOT to ensure the AI doesn't manalock itself by spamming it.
2017-09-14 19:12:02 +00:00
Agetian
3e2ef43a0a - [XLN] Added 7 cards by Marek.
- Some card script corrections by Marek.
2017-09-14 18:59:51 +00:00
Agetian
a73b480b1f - [XLN] Updated Enrage cards such that the trigger fires only once in case of simultaneous combat damage. 2017-09-14 13:43:21 +00:00
Agetian
4d374dd587 - Fixed a miswrite in laneswalker achievements, 2017-09-14 13:26:00 +00:00
Agetian
3b97f8c396 - Mobile Forge: fixed a long-standing bug which caused a match (e.g. Planar Conquest) to restart even after a victory when using Space or Enter shortcut keys (when running on PC or when using buttons on a mobile device that are bound to Space and/or Enter). 2017-09-14 09:42:17 +00:00
Agetian
e7a559327c - A tweak in CHANGES.txt. 2017-09-14 08:28:32 +00:00
Agetian
ede35abe1a - A tweak in CHANGES.txt. 2017-09-14 06:25:05 +00:00
Agetian
edc36f915d - Documenting changes in CHANGES.txt. 2017-09-14 06:24:42 +00:00
Agetian
05a9d457aa - Improvements to Counter AI. 2017-09-14 06:18:05 +00:00
Agetian
cc7ce6bae9 - Removed a debug print line in Scry AI. 2017-09-14 05:51:15 +00:00
Agetian
57e5135346 - Improvements to Scry AI. Also, made Scry AI configurable using several AI profile preferences. 2017-09-14 05:35:35 +00:00
Agetian
74980f84d5 - A more generic implementation of AF MustAttack. 2017-09-14 04:19:03 +00:00
Agetian
fa7f1cc21a - [XLN] Added Axis of Mortality, Brazen Buccaneers, Desperate Castaways, Dire Fleet Hoarder, Imperial Lancer, Skulduggery. Added some deck hints and technical info to some XLN scripts. 2017-09-14 03:56:05 +00:00
kevlahnota
8c90fb3c15 Planchase Mod for mobile Forge. Uses BG art of current planes when a player planeswalk. 2017-09-14 00:45:52 +00:00
kevlahnota
021a48c070 The string replacement for achievement collection is not needed. 2017-09-14 00:42:39 +00:00
Agetian
8be583083e - Added XLN planeswalker achievements. 2017-09-13 18:45:28 +00:00
Agetian
8d15a7249a - A fix in planeswalker achievement file. 2017-09-13 18:41:59 +00:00
Agetian
6259793f39 - Fixing a mistype in achievement file names. 2017-09-13 18:41:07 +00:00
Agetian
772faf8cd2 - Updated rankings for AKH and HOU. 2017-09-13 18:25:52 +00:00
Agetian
dd7141aeaa - Adding initial rankings for Iconic Masters. 2017-09-13 18:21:59 +00:00
Agetian
521a3f9341 - Adding Iconic Masters to blocks.txt.
- Adding Ixalan to blocks.txt (currently disabled until all the relevant information about the set is revealed).
2017-09-13 18:15:44 +00:00
Agetian
452bdd7b4f - NPE prevention in ControlGainAi. 2017-09-13 16:41:10 +00:00
Agetian
37ebb5731d - Added Ydwen Efreet.
- Forge is now at 100 unsupported cards total.
2017-09-13 16:35:11 +00:00
Agetian
933ce64cce - Fixed a NPE when trying to repeat last add card before anything has been actually added at least once. 2017-09-13 15:41:12 +00:00
Agetian
b8d0019ece - Added Dulcet Sirens. 2017-09-13 15:37:33 +00:00
Agetian
4df3da0856 - [XLN] Added Charging Monstrosaur, Kinjalli's Caller, Tilonalli's Knight. 2017-09-13 15:22:44 +00:00
Agetian
59f482e52e - Fixed one more logical error resulting in suboptimal/aggressive random trades. 2017-09-13 15:13:03 +00:00
Agetian
538ea82899 - AiBlockController: fixed a logic error in random trade code that led to overly aggressive trades 2017-09-13 15:09:12 +00:00
Agetian
0b7fc67c3f - Experimental: for Dingus Egg, do not check the time stamp of the card that changed zone (fixes interaction with Sacred Ground). Should this be the default behavior for ChangesZone triggers that do not seem to care for how many times the card had changed zones before the trigger resolves? 2017-09-13 14:52:09 +00:00
Agetian
7a92712f0b - As Foretold: allow to cast spells from command zone via its ability. 2017-09-13 14:48:50 +00:00
Agetian
6ed9a8b336 - A little tweak to the previous commit. 2017-09-13 14:25:44 +00:00
Agetian
24df4e78c7 - Random favorable trades on block: when evaluating face-down Morph of Manifested creature, evaluate it based on its original face, not the face down 2/2 one. 2017-09-13 14:12:32 +00:00
Agetian
a0abaf62b4 - [XLN] Added Trove of Temptation. This card is ugly, so certain side effects in corner cases are probably still possible. Currently implemented as a keyword-like ability similar to "no more than X creatures can attack each turn" etc., but maybe is better as a global rule, I'm not sure (and not sure how to properly convert it to a global rule either...). Assistance and improvements are welcome. 2017-09-13 08:59:35 +00:00
Agetian
85ece1ec63 - [XLN] Added Ruthless Knave. 2017-09-13 08:42:10 +00:00
Agetian
ba284aafe3 - [XLN] Added Snapping Sailback by azcotic. 2017-09-13 08:35:25 +00:00
Agetian
f25ab2f892 - [XLN] Added Lightning-Rig Crew, Raiders' Wake, Sword-Point Diplomacy 2017-09-13 08:32:30 +00:00
Agetian
b026111ef8 - Attempting to fix generation of keyword text with long descriptions. 2017-09-13 06:46:18 +00:00
Agetian
9850f2e242 - [XLN] Added Sky Terror. 2017-09-12 17:02:59 +00:00
Agetian
9313fa9193 - [XLN] Added Adanto Vanguard, Captivating Crew, Dire Fleet Ravager, River Sneak, Spell Swindle 2017-09-12 16:58:50 +00:00
Agetian
f19c9183f0 - Decouple MustAttackEntity from MustAttackEntityThisTurn for Alluring Siren. 2017-09-12 16:34:37 +00:00
Agetian
5af3384b02 - Improved implementation for Alluring Siren. 2017-09-12 11:07:46 +00:00
Agetian
46a5512209 - Fixed mana cost for Conqueror's Galleon. 2017-09-12 10:51:41 +00:00
Agetian
63c83e97c4 - [XLN] Added Makeshift Munitions and Vicious Conquistador. 2017-09-12 09:00:24 +00:00
Agetian
cf311fdeb1 - Fixed Alluring Siren implementation. 2017-09-12 08:16:54 +00:00
Agetian
04ef82bc70 - [XLN] Added Legion Conquistador. 2017-09-12 03:26:04 +00:00
Agetian
7182ad2e9c - Some more NPE prevention in getSpellAbilityPriority. 2017-09-12 02:56:34 +00:00
Agetian
072508e1c1 - Added a TODO comment. 2017-09-11 19:12:47 +00:00
Agetian
91101a7c40 - Added a TODO comment 2017-09-11 19:12:33 +00:00
Agetian
d34fa71797 - [XLN] Added Dowsing Dagger / Lost Vale, Legion's Landing / Adanto, the First Fort. 2017-09-11 18:04:42 +00:00
Agetian
bee7bccc12 - A couple XLN card fixes / updates. 2017-09-11 17:48:44 +00:00
Agetian
74eea096b1 - Added some more experimental combat AI options, currently disabled by default.
- Enabling some of the previously experimental, now tested combat AI options (random favorable trades, holding combat tricks until block, trading to save a planeswalker, attempting to avoid attacking into certain blocks).
2017-09-11 17:37:43 +00:00
Indigo Dragon
02c028584b Added Iconic Masters.txt 2017-09-11 15:17:04 +00:00
Agetian
2a87d338b4 - Removed a superfluous comment. 2017-09-11 14:17:19 +00:00
Agetian
8759532964 - Experimental: in combat AI, try to avoid counting static abilities that grant power bonuses twice in case the creature has already attacked and the relevant bonus is already accounted for in getNetPower/getNetToughness (aims to fix AI prediction of creature power in presence of cards such as War Horn). 2017-09-11 14:16:02 +00:00
Agetian
f332db93ad - Added a couple final declarations in PumpAi. 2017-09-11 11:08:21 +00:00
Agetian
6ffc687174 - Some improvements to Aristocrats PumpAi and Electrostatic Pummeler AI.
- Added some final declarations in SpecialCardAi.
2017-09-11 11:06:39 +00:00
Agetian
ba6079164c - Attempting to fix a concurrent exception on an accidental quick reentry of addAttackingBand. 2017-09-11 09:55:26 +00:00
Agetian
372ee945de - Added puzzle PC_122215 coded by Xitax. 2017-09-11 04:32:41 +00:00
Agetian
0f0eed3de6 - A minor tweak/fix in As Foretold script code. 2017-09-10 16:54:26 +00:00
Agetian
9eddb37c3e - As Foretold: a simpler check for split half CMCs. 2017-09-10 16:50:19 +00:00
Agetian
6a262ea604 - A hacky workaround for the interaction between As Foretold and split cards. Seems to work in general cases, for both non-Aftermath and Aftermath splits, but a better solution is most certainly needed and welcome. 2017-09-10 16:25:32 +00:00
Agetian
94693f530e - Documenting changes in CHANGES.txt. 2017-09-10 15:11:33 +00:00
Agetian
ee9eced602 - Integrating PTK Quest world by Xyx. 2017-09-10 15:09:05 +00:00
Agetian
58fa9b8182 - Added a known issue note to As Foretold card text. 2017-09-10 14:40:05 +00:00
Agetian
89b377ab8f - Added a difficult to resolve known issue about As Foretold to ISSUES.txt. 2017-09-10 14:36:50 +00:00
Agetian
1d93fccbb3 - As Foretold: simplified implementation of copyWithDefinedMana 2017-09-10 13:42:16 +00:00
Agetian
f8786bcd47 - Ancestral Vision and similar cards should also be castable without paying their mana cost (117.6a). 2017-09-10 11:46:34 +00:00
Agetian
8adf2043f7 - Added puzzle PS_HOU8 coded by Nigol. 2017-09-10 11:35:46 +00:00
Agetian
8af3fe3db8 - Attempting to fix an issue with fastlands and slowlands ETBing in the wrong state when entering the battlefield together with other lands at the same time.
- Fixed an issue with LastStateBattlefield/LastStateGraveyard not returning anything unless at least some ability has been played.
2017-09-10 11:32:42 +00:00
Agetian
09c1db9afe - As Foretold: allow interaction with spells that initially have no mana cost (e.g. Ancestral Vision). 2017-09-10 11:28:07 +00:00
Indigo Dragon
2f738ff2b7 Updated Rarity corrections 2017-09-10 08:07:16 +00:00
Agetian
4429e36c3f - Improved copyWithDefinedMana such that the defined mana cost is added to spells that initially have no mana cost.
- TODO: it's still impossible to cast e.g. Ancestral Vision via As Foretold, even though it should be possible according to a ruling. Not sure how to fix, help is welcome.
2017-09-10 07:53:27 +00:00
Agetian
0b8a3cfcea - Emblems should be colorless. 2017-09-10 06:19:07 +00:00
Agetian
c3d43f6021 - Use CARDNAME in the desc of As Foretold. 2017-09-10 06:02:28 +00:00
Agetian
2a5badbc7c - Added As Foretold. 2017-09-10 05:54:43 +00:00
Agetian
3c5f24f99d - Fixed Shefet Monitor text. 2017-09-10 03:23:18 +00:00
Agetian
105212a667 - Fixed interaction between Animate Dead and Worldgorger Dragon. 2017-09-10 03:21:49 +00:00
Agetian
f5decd2221 - Improvement to the PumpAI Aristocrat logic. 2017-09-09 17:15:41 +00:00
Agetian
48bd65cd3b - Do not double the Infect damage when predicting it (already accounted for later) 2017-09-09 16:07:29 +00:00
Agetian
90046b11af - Card name fix. 2017-09-09 15:51:23 +00:00
Agetian
6eede614f8 - [XLN] Added Lurking Chupacabra, Wildgrowth Walker. 2017-09-09 15:51:07 +00:00
Agetian
8cd9c5e8a3 - Minor formatting fix. 2017-09-09 15:35:46 +00:00
Agetian
0ef1019d1c - Implemented a simple logic for Aristocrats (Vampire Aristocrat, Bloodthrone Vampire, Nantuko Husk and friends).
- Improved prediction of Infect damage for the AI in shouldPumpCard.
2017-09-09 15:34:35 +00:00
kevlahnota
f2a55e525b Enhanced rotated display of Planes/Phenomenon 2017-09-09 13:35:37 +00:00
kevlahnota
0f9eecd821 Added UI Setting (Mobile Forge) for rotated zoomed image display for Planes and Phenomenon cards. 2017-09-09 10:20:06 +00:00
Indigo Dragon
a45848cbe3 Added reminder text to chief_engineer.txt and maelstrom_nexus.txt.
Also changed modular so that it now has the correct "dies" wording on trigger.
2017-09-09 10:05:35 +00:00
Agetian
07af7d7f97 - [XLN] Renamed Shapers of the Nature -> Shapers of Nature 2017-09-09 04:53:56 +00:00
Agetian
87186ca940 - Territorial Hellkite: fixed Oracle text. 2017-09-09 03:35:29 +00:00
Agetian
990bd7e291 - AiController: attempting to fix a "Comparison.sort violates general contract" error which is likely caused by an internal NPE inside the sort method (difficult to prove if this really fixes the issue since I can't reproduce the problem yet, though it had been reported twice already). 2017-09-09 03:14:34 +00:00
Agetian
c39aa35d36 - Territorial Hellkite: further tweaks to the implementation for the corner cases where it can't attack or attacks someone else. 2017-09-09 03:00:15 +00:00
Agetian
3b187d75bb - Territorial Hellkite: improve interaction when it can't legally attack. 2017-09-08 20:06:30 +00:00
Agetian
84694773a2 - Added Territorial Hellkite. 2017-09-08 19:55:51 +00:00
Agetian
e8d9083f05 - [XLN] Added Bishop of the Bloodstained, Merfolk Branchwalker.
- Added some deck hints to XLN cards.
- Minor corrections in a couple XLN cards.
2017-09-08 18:49:56 +00:00
Agetian
25ccebe617 - [XLN] Added Rile. 2017-09-08 16:55:45 +00:00
Agetian
c6d3d07b3c - [XLN] Added Vona, Butcher of Magan. 2017-09-08 16:34:51 +00:00
Agetian
c6078f9314 - [XLN] Fixed the -3 ability for Vraska, Relic Seeker. 2017-09-08 16:26:34 +00:00
Agetian
db5462b79f - [XLN] Added Vraska, Relic Seeker. 2017-09-08 16:26:08 +00:00
Agetian
3b8e6a2c3a - [XLN] Added Dinosaur Stampede, Rampaging Ferocidon, Raptor Hatchling. 2017-09-08 16:18:07 +00:00
Agetian
eae7e79ecf - Added SpellDescription to an ability in Search for Azcanta / Azcanta, the Sunken Ruin. 2017-09-08 16:04:51 +00:00
Agetian
5264873645 - [XLN] Added Search for Azcanta / Azcanta, the Sunken Ruin.
- Minor fix in Welcome to the Fold.
2017-09-08 16:03:21 +00:00
Indigo Dragon
6009f097cd Fixed some Preconstructed Intro decks 2017-09-08 15:49:01 +00:00
Agetian
96265e5ac1 - Added a couple cards to the AI's library in Pauper Puzzle #03 to avoid a possibility of decking the opponent. 2017-09-08 12:41:01 +00:00
Agetian
5ee33a35fc - Lazav, Dimir Mastermind: copy the non-LKI card (fixes interaction with cards dying to something like Nameless Inversion; however, I'm not sure if LKI was intentional here or not) 2017-09-08 12:40:11 +00:00
Agetian
55f3c484f2 - [XLN] Added Lookout's Dispersal, Ranging Raptors, Ravenous Daggertooth, Storm Fleet Spy 2017-09-08 12:37:33 +00:00
Agetian
0bd678318c - Fixed Otepec Huntmaster. 2017-09-08 11:58:42 +00:00
Indigo Dragon
89ab049391 Changed some keyword reminder texts so that they are more dynamic with {%d:numbers).
Also updated suspend reminder text in CardFactoryUtil. It used to have this weird stock "Three Time Counters". Now it's fixed. Except it has Suspend counters in "1 counters" instead of "a counter"/"3 counters" instead of "three counters". Any improvements are welcome.
2017-09-08 11:07:58 +00:00
Indigo Dragon
aa6e4c5b53 Added reminder texts for Delve, Dredge, Split second, Devoid, Totem armor, Afflict, Bushido, Poisonous, Rampage, Cascade, Conspire, Dredge (again), Flashback*.
*Flashback required changes to the actual Flashback code. Now it works with an em-Dash. I'll possibly apply similar changes to other keywords with em-Dash cast mode.
2017-09-08 08:24:46 +00:00
Agetian
ec8a75be52 - Fixed Burning Sun's Avatar Oracle text. 2017-09-08 04:42:05 +00:00
Agetian
3ae7fe7f04 - Added Arguel's Blood Fast / Temple of Aclazotz, Chart a Course, Commune with Dinosaurs, Fell Flagship, Grazing Whiptail, Growing Rites of Itlimoc / Itlimoc, Cradle of the Sun, Otepec Huntmaster, Skittering Heartstopper, Thundering Spineback.
- Temporary implementation of an Explores trigger linked to AB Dig (until AB Explore is implemented).
2017-09-08 04:41:35 +00:00
kevlahnota
3e4b5830be Fixes Warning in Log - unmappable character for encoding UTF-8 2017-09-07 22:43:28 +00:00
Agetian
8ea28dad68 - [XLN] Added Duskborne Skymarcher. 2017-09-07 18:20:56 +00:00
Agetian
17c1fc79e1 - Fixed spells with Bestow not getting "unbestowed" when they are countered and go to graveyard. 2017-09-07 18:08:51 +00:00
Agetian
801eaaaf37 - A couple minor fixes. 2017-09-07 13:10:56 +00:00
Indigo Dragon
bfd72d7540 Added capital letters to the Protection cards with unique protections 2017-09-07 11:04:59 +00:00
Agetian
1341ef0c3e - Updated token-images.txt 2017-09-07 05:41:21 +00:00
Agetian
95bda0d4bb - Preparing Forge for Android publish 1.6.2.007 [hotfix]. 2017-09-07 05:34:09 +00:00
Agetian
adf412ea74 - Removed SacMe from Puzzleknot cards, doesn't work exactly as was expected. 2017-09-07 05:32:08 +00:00
Agetian
a00a6734b3 - Further tweaks to surprise pump experimental feature. 2017-09-07 05:27:33 +00:00
Agetian
e410eab336 - Fixed Priest of the Wakening Sun, improved default prompt for UnlessCost that is switched. 2017-09-07 03:50:28 +00:00
Agetian
1367357e68 - Updated puzzle PC_120815. 2017-09-06 19:10:03 +00:00
Agetian
4cb4b3bb19 - Preparing Forge for Android publish 1.6.2.006 [incremental]. 2017-09-06 18:04:00 +00:00
Agetian
ce92ab77ec - Enable a simple AI discard cost preference for Heir of Falkenrath. 2017-09-06 18:02:34 +00:00
Agetian
e365fc60bc - Removed Favorable Winds from upcoming (reprinted card). 2017-09-06 17:43:56 +00:00
Agetian
edb1343676 - For the time being, hiding the latest change to attack controller behind an experimental option, enabled in the experimental profile (may be defaulted and removed as a profile option later after extensive testing). 2017-09-06 16:51:32 +00:00
Agetian
f1426103ff - An important tweak to the previous commit. 2017-09-06 16:43:50 +00:00
Agetian
7a767327c0 - AiAttackController: don't attack into a guaranteed block unless the attacker has some kind of an attack/combat effect. 2017-09-06 16:21:49 +00:00
Agetian
e2f9139aa9 - One more tweak to the dev menu (mobile Forge). 2017-09-06 15:40:46 +00:00
Agetian
a80b504379 - A couple tweaks to dev panel. 2017-09-06 15:40:07 +00:00
Agetian
d73b3c44e3 - More work and parameter tweaking for attack/block trade experimental AI. 2017-09-06 15:32:24 +00:00
Agetian
c05f54c105 - Fixed non-UTF-8 encoding in CardFactoryUtil. 2017-09-06 15:25:20 +00:00
Agetian
c917bafde5 - [XLN] Added Regisaur Alpha. 2017-09-06 15:18:53 +00:00
Agetian
b349106f08 - Ixalan creature type update (Dinosaur subtype according to Sep 6 notes) 2017-09-06 15:16:14 +00:00
Indigo Dragon
1c91f345fc Added em-Dash for Suspend 2017-09-06 15:03:49 +00:00
Agetian
2b11afbb2d - More tweaks to the attack/block trading experimental options. 2017-09-06 14:30:24 +00:00
Agetian
e9479dd24f - Using em-dash for Awaken generated description. 2017-09-06 14:24:29 +00:00
Agetian
d676412291 - Fixed description generation for KW Devour. 2017-09-06 14:19:49 +00:00
Indigo Dragon
5fd5d032ef Updates to Keywords again 2017-09-06 13:56:27 +00:00
Agetian
66ee09f656 - Added NeedsToPlayVar to Explore. 2017-09-06 13:50:29 +00:00
Indigo Dragon
8f57a55286 Updates to keywords
So it's not the End of the World
2017-09-06 12:47:46 +00:00
Agetian
6498250114 - NPE prevention in CardZoom (still need to figure out why sometimes [seemingly randomly] the card on stack will become "castable" in mobile Forge in the first place). 2017-09-06 12:06:12 +00:00
Indigo Dragon
bf7b9a7057 CHANGES TO KEYWORDS !!!DANGER!!!
I'm messing with powers I don't understand. Here I go.

Following changes to menace that removed reminder text by removing the keyword.equals("Menace"), I conclude that you can add reminder text by inserting the keyword.equals("Keyword"). This is part one if it works, as it is for all the simple keywords that don't rely on numbers or costs (Compare Devoid with Awaken)

If this doesn't work, and everything breaks, burn it.
2017-09-06 12:06:01 +00:00
Agetian
8de7099c0f - A little tweak to the "attack to trade" experimental code. 2017-09-06 11:36:02 +00:00
Agetian
c2b3c4919d - [XLN] Added Favorable Winds, Storm Fleet Aerialist, Storm Fleet Arsonist. 2017-09-06 11:26:55 +00:00
Indigo Dragon
90b5531041 Updated Keyword Reminder Texts
Notes: Technically, all the reminder texts here were the texts as described by Magic Comprehensive Rules 702. Keyword Abilities. However nobody actually follows the rules when making simple reminder text for actual cards. If people are confused over advanced situations where creatures become noncreature permanents but the reminder text specifies "creature", they should consult the overriding Comprehensive rules. Therefore; I feel justified in simplifying these 'reminder' texts.
2017-09-06 10:47:33 +00:00
Indigo Dragon
e57de3268f illusionists_stratagem.txt Removed double "Draw a Card."
street_spasm.txt Added a full stop.
2017-09-06 09:46:35 +00:00
Agetian
b9e68834de - A little improvement in Suggest basic land count feature. 2017-09-06 08:50:46 +00:00
Agetian
5352514c2b - KeepTapped Attach AI logic: try not to activate on things that can't tap. 2017-09-06 05:07:49 +00:00
Agetian
aa58b3df35 - [XLN] Added a couple deck hints. 2017-09-06 04:37:42 +00:00
Agetian
dfe5a90b06 - [XLN] Added Savage Stomp and Wily Goblin. 2017-09-06 04:36:06 +00:00
Agetian
893486eb93 - Reorganized addCardToZone a little bit. 2017-09-06 04:35:38 +00:00
Agetian
bdc684d2d1 - Fixed Dev mode Add Card to X functions. 2017-09-06 04:14:24 +00:00
Agetian
80bc435df6 - Added Dinosaur creature subtype to lists/TypeLists.txt 2017-09-06 03:34:44 +00:00
kevlahnota
00cbfa45d7 Minor tweak in InputSelectTargets and some string refactoring 2017-09-05 20:53:54 +00:00
Agetian
5c18270826 - ComputerUtilCost: sac costs (Sac<...>) use ";" as a delimiter and not "," (fixes e.g. the AI never sacrificing anything to Defiant Salvager).
- Added SacMe to Puzzleknot cards.
2017-09-05 19:35:35 +00:00
Agetian
aa86ef631b - DeckGenUtil: some improvements to the suggest basic lands feature. 2017-09-05 19:09:06 +00:00
Agetian
c67a2bd458 - Added an Explores trigger (not used anywhere yet, can be used in the upcoming Explore effect later). 2017-09-05 18:05:33 +00:00
Agetian
f796ec32a7 - [XLN] Added Deadeye Quartermaster, Kitesail Freebooter, Overflowing Insight, Pirate's Cutlass. 2017-09-05 17:55:13 +00:00
Agetian
9ce43ea7a6 - [XLN] Added 5 cards. 2017-09-05 16:19:42 +00:00
Agetian
19702794e4 - Volcano Hellion: tweak the script such that it allows to choose any number. 2017-09-05 15:25:30 +00:00
Agetian
9015bfcac3 - Minor dev panel button movement. 2017-09-05 12:12:18 +00:00
Agetian
ad13ef9187 - Dev Mode: split the "Add Card to Play" functionality into two buttons: "Add Card to Battlefield", which acts like other Add Card to X buttons and adds the card directly to the battlefield, without using the stack and without firing ETB triggers; and "Cast Spell/Play Land", which acts like the old "Add Card to Play" button and uses the stack when necessary and fires all triggers. 2017-09-05 12:06:37 +00:00
Agetian
4a1b147a25 - A little improvement in script execution code in GameState. 2017-09-05 03:37:33 +00:00
Agetian
5970908a3f - GameState: added a way to precast from a custom script line for the needs of Puzzle Mode. 2017-09-05 03:35:52 +00:00
Agetian
e3d37ca831 - Reverted an inadvertent experimental commit. 2017-09-05 03:27:29 +00:00
Agetian
4c9afb8c80 - Fixed puzzle PP18. 2017-09-05 03:27:08 +00:00
Agetian
253049ec5a - [XLN] Added Shapers of the Nature. 2017-09-05 03:11:45 +00:00
Agetian
c1d7f86542 - [XLN] Added Drover of the Mighty. 2017-09-05 03:04:56 +00:00
Agetian
aa44fba1e5 - Added 2 puzzles coded by Xitax, 9 puzzles coded by Nigol. 2017-09-05 02:53:28 +00:00
kevlahnota
5dbf2b0ff5 Fix font display in landscape mode (instead of vertical names, it will display names horizontall) Edited 2017-09-05 02:47:55 +00:00
kevlahnota
8fcbaef064 Refactor some strings 2017-09-05 02:44:36 +00:00
Agetian
08c3509823 - [XLN] Added some more cards. 2017-09-04 18:55:56 +00:00
Agetian
d9961bc1a0 - [XLN] Added some more cards + patched up a couple cards a bit. 2017-09-04 18:55:14 +00:00
Agetian
9fd8c1f546 - Basic Survival of the Fittest logic (in addition to it already accounting for DiscardMe cards), most likely needs tweaking. Promoted Survival of the Fittest to RemRandomDeck from RemAIDeck. 2017-09-04 17:32:46 +00:00
Agetian
d265102f31 - [XLN] Added Wakening Sun's Avatar. 2017-09-04 16:20:34 +00:00
Agetian
063a50b14a - Some modification of chance variables in the AI profiles related to the experimental attack/block trade options. 2017-09-04 13:24:56 +00:00
Agetian
ebb06b34b0 - Some more work on attack/block trades [experimental]. 2017-09-04 12:18:06 +00:00
Agetian
bbc3a75e4a - A NPE guard in the experimental pump code. 2017-09-04 09:59:07 +00:00
Agetian
86c5ef363e - A tweak in the lure pump experimental code. 2017-09-04 09:00:23 +00:00
Agetian
daefcab8f5 - A little modification for the previous commit. 2017-09-04 05:42:41 +00:00
Agetian
3d5f9beac5 - ChangeZoneAi: implemented AILogic SacAndRetFromGrave, added it to Recurring Nightmare. 2017-09-04 05:40:27 +00:00
Agetian
0b01855bf4 - Maniacal Rage: promoted to RemRandomDeck. 2017-09-04 04:58:59 +00:00
Agetian
cc78904582 - GameState: support for targeting scripted abilities that use a Defined$ parameter originally (for Puzzle Mode needs). 2017-09-04 04:32:25 +00:00
Agetian
306b4652eb - Fixing a potential crash in the experimental holdCombatTricks code. 2017-09-04 04:10:41 +00:00
Agetian
721a629ba8 - LifeInDanger AI: account for special cases with Worship and Elderscale Wurm 2017-09-03 18:58:51 +00:00
Agetian
4f00aeeed7 - Fixed a quest opponent deck file. 2017-09-03 15:57:32 +00:00
Agetian
88ca671965 - Some more work on surprise attacks/trades [experimental, disabled by default]. 2017-09-03 15:26:28 +00:00
Indigo Dragon
7c3cfe1326 Updated all the Overload Reminder Texts
Some now use ValidDescription$
2017-09-03 15:05:02 +00:00
Agetian
735f1007cf - AB$ Cost 0 -> DB$ in Festering Mummy. 2017-09-03 14:44:57 +00:00
Agetian
66df063e2a - Added ValidDescription to Street Spasm. 2017-09-03 14:44:35 +00:00
Indigo Dragon
e7547905a3 Magus of the Wheel had a weird Name:Meteor Blast in its oracle text 2017-09-03 13:59:08 +00:00
Agetian
206b01376d - Added unfinished and inoperable Online Play mode to ISSUES.txt (known issues). 2017-09-03 13:04:03 +00:00
Agetian
e50ef893e8 - [XLN] Added Huatli, Warrior Poet. 2017-09-03 12:55:47 +00:00
Indigo Dragon
496cc470e2 Update all the Prowl and Reinforce cards from MOR.
They now pretend to be keywords better.
2017-09-03 11:54:55 +00:00
Agetian
b82516ba4d - Minor improvement related to my previous commit. 2017-09-03 11:19:23 +00:00
Agetian
bbe7ff3c9b - Further work on surprise pump spells on attack [experimental, disabled by default, may still cause issues when enabled]. 2017-09-03 11:12:48 +00:00
Indigo Dragon
d33770f107 Update Convoke Reminder text to include the "(Your creatures can help cast this spell. ", similar to Improvise.
Also updated Frenzy Sliver and Virulent Sliver. Frenzy Sliver is done, while Virulent sliver needs a few more tweaks in an upcoming Keyword update.
2017-09-03 09:50:04 +00:00
Indigo Dragon
6523500d09 Update arcanum_wings.txt so that it now acts more like a keyword.
PrecostDesc$ -> Costdesc$, Added forward brackets and costs to Spelldescription$
2017-09-03 09:29:13 +00:00
Agetian
1a7a5fe6a8 - Fixed some AB$ Cost 0 -> DB$ declarations in card scripts.
- Fixed Nova Pentacle (the card needs some additional combat AI support though, or the AI will suicide its own creatures if the opponent has nothing on the battlefield).
2017-09-03 08:55:26 +00:00
kevlahnota
9426c531e3 More Refactoring of String.format 2017-09-03 08:11:28 +00:00
Indigo Dragon
35a6977df8 Gave seeds_of_renewal.txt the Undaunted Keyword in the Oracle text 2017-09-03 08:05:10 +00:00
Agetian
c06280a1ea - [XLN] Added 3 cards. 2017-09-03 04:59:10 +00:00
kevlahnota
8b3ff137d1 Refactor strings enclosed in parentheses, transform P/T to strings 2017-09-03 02:01:34 +00:00
Agetian
0fa826f926 - Minor formatting tweak. 2017-09-02 18:26:10 +00:00
Agetian
5c7b40248c - A more generic solution for concatWithSpace / concatNoSpace. 2017-09-02 18:24:30 +00:00
kevlahnota
7a3a8b0490 Refactor some "String.format" to use StringBuilder 2017-09-02 17:35:13 +00:00
Agetian
78d13749d5 - AbilityUtils: fixed compilation (please check, replaced CardUtil.getPluralType with CardType.getPluralType) 2017-09-02 17:06:12 +00:00
Agetian
1143a43d30 - Attempting to improve LifeGain AI for creatures with negative SA activation costs like Spike Feeder. 2017-09-02 17:00:10 +00:00
Hanmac
3cdf553142 moved hardcoded type plural into text file and moved the getPlural and getSingular functions into CardType class 2017-09-02 16:55:11 +00:00
Agetian
f9569895b4 - Hermit Druid: added AILogic$ DontMillSelf 2017-09-02 16:31:07 +00:00
Indigo Dragon
905b469515 Updated Eternalize keyword so now it's similar to Embalm, as well as specifying that it gains the Zombie type, not replaces 2017-09-02 14:53:15 +00:00
Indigo Dragon
aac3443b78 Updated Madness Reminder Text 2017-09-02 14:46:44 +00:00
Indigo Dragon
bd9b05463e Changing the mana cost 2 to {2} for the [[Rout]] ability
K:You may cast CARDNAME as though it had flash if you pay 2 more to cast it. -> K:You may cast CARDNAME as though it had flash if you pay {2} more to cast it.

This may break something. If so, whoops
2017-09-02 14:27:29 +00:00
Agetian
bb82c6fa02 - Further work on trick/lure attacks with held pump spell. 2017-09-02 14:19:34 +00:00
Indigo Dragon
7232f9446a Replaced all double spaces __ with single spaces _ 2017-09-02 14:17:43 +00:00
Agetian
639f2b8e02 - [XLN] Fixed Fathom Fleet Captain 2017-09-02 13:50:21 +00:00
Agetian
7d860542b1 - [XLN] Added Bellowing Aegisaur. 2017-09-02 12:55:17 +00:00
Agetian
7593dad6c8 - Some more improvements for block-baiting into a pump. 2017-09-02 11:32:52 +00:00
Agetian
ecc4834110 - Adding hyphenated emblem names to the rest of the planeswalker files. 2017-09-02 11:11:57 +00:00
Agetian
2e1adc6f9b - Documenting changes in CHANGES.txt. 2017-09-02 11:08:12 +00:00
Agetian
18a0866735 - Code support for hyphens in Planeswalker emblem names. 2017-09-02 11:05:51 +00:00
Indigo Dragon
4036fc44e3 More brackets [] and colons : for planeswalkers
That should be it for now.
2017-09-02 11:05:25 +00:00
Indigo Dragon
9f7bbd6efc Upgrade Tamiyo, the Moon Sage ai emblem name 2017-09-02 10:41:45 +00:00
Indigo Dragon
1ff11a09ed Added Colons to Planeswalkers
Added dashes to some Emblem Names
Added Brackets for some select walkers
2017-09-02 10:39:51 +00:00
Hanmac
be3d4723ef Emblem fixed startwith 2017-09-02 10:28:07 +00:00
Agetian
190703d74b - Some changes in PumpAi related to the in-dev experimental features. 2017-09-02 10:05:17 +00:00
Indigo Dragon
eac361d892 Gave Colons to kytheon_hero_of_akros.txt 2017-09-02 06:32:10 +00:00
Indigo Dragon
a772cf9910 Changes to Emblems
Emblems now read "Emblem Planeswalker" instead of "Planeswalker emblem"
Image name now is "emblem_planeswalker"

If this breaks anything please revert.

Also added some colons to oracle texts for some planeswalkers.
2017-09-02 05:52:02 +00:00
Indigo Dragon
77eab9e6f8 Changed vessel_of_malignity.txt 2017-09-02 04:44:30 +00:00
Agetian
c4fc168369 - Minor comment fix. 2017-09-02 04:00:29 +00:00
Agetian
41409fb5c1 - An AI option to hold combat tricks until block, experimental and disabled by default (more work to follow soon). 2017-09-02 03:57:06 +00:00
Agetian
71eb88eb7b - Fixed Claim // Fame. 2017-09-02 03:03:27 +00:00
Agetian
dcbde94d78 - [XLN] Added 8 cards from Planeswalker decks. 2017-09-01 17:09:38 +00:00
Agetian
11bbcf5b42 - More work on "surprise trade on block" project (experimental, disabled by default). 2017-09-01 15:45:17 +00:00
Agetian
badf68b80a - Further work on random trades (experimental, disabled by default.) 2017-09-01 14:53:44 +00:00
Agetian
01e1c2ab0a - Added Prowess, Outlast and Afflict to CreatureEvaluator. 2017-09-01 14:02:55 +00:00
Agetian
bef51732e0 - Fixed an inverted clause in the experimental "random combat trades" code. 2017-09-01 13:42:12 +00:00
Agetian
77046cf38a - Fogwalker: removed empty lines, AB with cost 0 -> DB 2017-09-01 13:35:24 +00:00
Agetian
2a623957c3 - A little tweak to the previous commit. 2017-09-01 08:49:30 +00:00
Agetian
65322925a4 - Pump effects: consider beneficial combat pump effects when checking if the AI should pump the card. Also, use "gains flying until EOT" defensively if there's a chance to kill the opponent's creature or block the opposing flyer with an indestructible flying creature. 2017-09-01 08:31:50 +00:00
Agetian
346c4db3d9 - Whispers of the Muse, Treasure Trove: added AILogic$ AlwaysAtOppEOT as it's a more optimal default timing for this effect. 2017-09-01 07:56:42 +00:00
Agetian
7212698863 - Fabrication Module: added AILogic. 2017-09-01 07:29:06 +00:00
Agetian
1779544d70 - Further work on experimental attack/block trade feature (disabled by default). 2017-09-01 05:54:01 +00:00
Agetian
0270420da8 - Some minor refactoring in AiBlockController related to the previous commit. 2017-09-01 03:22:11 +00:00
Agetian
a1461851ee - Some further work on the experimental attack/block trading options (disabled by default in all profiles except Experimental). 2017-09-01 03:19:24 +00:00
Agetian
eaabad923e - Reverted a change to Edgar Markov that breaks it for tribal Vampire spells. 2017-09-01 03:10:17 +00:00
Agetian
a9e12f5aca - Added final to several vars in PlayerControllerHuman. 2017-08-31 19:14:55 +00:00
Agetian
0637d5511d - Some experimental attacking and blocking changes related to surprise trade attacks and blocks, currently disabled by default in all profiles except Experimental. 2017-08-31 19:11:04 +00:00
Agetian
27682b2d07 - Electrostatic Pummeler AI: predict static damage prevention 2017-08-31 18:05:30 +00:00
Agetian
0982852563 - Fixed Nazahn, Revered Bladesmith. 2017-08-31 12:51:08 +00:00
Agetian
8147a315c2 - Added rudimentary AI to requested cards Erratic Portal and Null Brooch. Helm of Possession appears to be AI playable already after testing, so marking it as such. 2017-08-31 12:41:54 +00:00
Agetian
fdf787e9b4 - Integrating latest Oracle updates by Indigo Dragon. 2017-08-31 09:52:40 +00:00
Agetian
01d2338388 - [XLN] Added Queen's Bay Soldier. 2017-08-31 08:46:45 +00:00
Agetian
1dfec40f49 - [XLN] Added Hostage Taker, Sorcerous Spyglass and Tishana, Voice of Thunder. 2017-08-31 05:08:08 +00:00
Agetian
a82602faae - [XLN] Added Star of Extinction and Vanquisher's Banner. Fixed the card name in Shapers' Sanctuary. 2017-08-31 04:22:03 +00:00
Agetian
c2aac371ae - Cleaned up imports. 2017-08-30 19:28:08 +00:00
Agetian
9aa6e0d39a - CloneAi: improved a bit for Tilonalli's Skinshifter. 2017-08-30 19:27:12 +00:00
Agetian
3c177c4ebf - [XLN] Added Angrath's Marauders, Captain Lannery Storm, Sunbird's Invocation, Tilonalli's Skinshifter. 2017-08-30 19:13:46 +00:00
Agetian
d8b090bae2 - Added quest opponents Jafar 2 (mono B zombies with some Afflict) and Heinz Doofenshmirtz 2 (URG Electrostatic Pummeler). 2017-08-30 18:29:20 +00:00
Agetian
928792bea7 - Some method renaming. 2017-08-30 17:17:10 +00:00
Agetian
99775194fd - Electrostatic Pummeler AI: teach the AI to pump the Pummeler to save it from dying to direct damage spells. 2017-08-30 17:16:00 +00:00
Agetian
beb73828e0 - LifeSetAi: Do not use this ability in case the amount the life is set to is the same as the AI's current life (e.g. Oketra's Last Mercy while the AI is already at the starting 20 life). 2017-08-30 16:52:20 +00:00
Agetian
87dc72b874 - [XLN] Added Bloodcrazed Paladin, Boneyard Parley, Fathom Fleet Captain, Ruin Raider, Vraska's Contempt. 2017-08-30 16:12:27 +00:00
Agetian
528ae68282 - Preparing Forge for Android publish 1.6.2.005 [incremental]. 2017-08-30 15:18:49 +00:00
Agetian
a44031cd39 - [XLN] Added Old-Growth Dryads, Shaper's Sanctuary coded by azcotic 2017-08-30 15:14:55 +00:00
Agetian
af922d5171 - Added Deeproot Champion and Waker of the Wilds coded by azcotic [fixed versions]. 2017-08-30 15:06:43 +00:00
Agetian
bd6599a309 - Better Electrostatic Pummeler AI. 2017-08-30 14:55:01 +00:00
Agetian
2e062b1ad6 - [XLN] Added Kopala, Warden of Waves and River's Rebuke. 2017-08-30 05:45:48 +00:00
Agetian
5fc6654045 - [XLN] Added Arcane Adaptation, Daring Saboteur, Dreamcaller Siren, Entrancing Melody, Herald of Secret Streams. 2017-08-30 05:30:59 +00:00
Agetian
32c3494098 - Added Know Evil. 2017-08-30 04:49:13 +00:00
Agetian
379461820b - Adhering to Java 7 feature set for the purpose of Android compatibility. 2017-08-30 03:19:23 +00:00
Agetian
424cb7a9f2 - [XLN] Fixed Admiral Beckett Brass card name. 2017-08-30 03:17:19 +00:00
Agetian
b41e2b6f51 - Electrostatic Pummeler AI: don't overpump when using it defensively. 2017-08-29 16:56:42 +00:00
Agetian
84e73f6f52 - Fixed the AI cheating with Plague Belcher by ignoring its triggered ETB ability. 2017-08-29 16:18:09 +00:00
Agetian
39fb42db10 - Enabling evasion prediction for assault and attrition attack declarations in default AI profiles after testing. 2017-08-29 16:05:20 +00:00
Agetian
85c18ce07b - Somewhat better RememberedWithSharedCardTypes, hopefully would work more reliably in case of 3+ cards tested at the same time for shared card type [not currently seen on any card]. 2017-08-29 15:57:30 +00:00
Agetian
3fe548d8b7 - [XLN] Added Rowdy Crew. 2017-08-29 15:49:30 +00:00
Agetian
eb32222d39 - [XLN] Added Ashes of the Abhorrent, Mavren Fein Dusk Apostle, Sanguine Sacrament, Settle the Wreckage, Tocatli Honor Guard.
- [XLN] Added some AI flags to cards.
- Fixed Hymn of the Wilds description.
2017-08-29 14:32:22 +00:00
Agetian
e70d7ab5c1 - [XLN] Added Deadeye Tormentor, Prosperous Pirates, Sun-Crowned Hunters. 2017-08-29 09:30:05 +00:00
Agetian
a3da735087 - [XLN] Revel in Riches: the effect is not optional. 2017-08-29 09:18:04 +00:00
Agetian
8c12b5a47e - Removed a debug print line. 2017-08-29 09:10:30 +00:00
Agetian
65c27c6c93 - Fixed an issue with non-creature tokens being added twice to different rows, resulting in odd Z order when mousing over them. 2017-08-29 09:03:36 +00:00
Agetian
4c31e1a4b5 - Minor counter abbreviation tweak. 2017-08-29 08:33:25 +00:00
Agetian
7be776d417 - Added spell description to Treasure Map. 2017-08-29 05:56:29 +00:00
Agetian
1a93e8f331 - [XLN] Added Revel in Riches, Sleek Schooner, Treasure Map // Treasure Cove.
- Added Picture SVars to card in Upcoming.
- Fixed Emperor's Vanguard (should work well enough until Hanmac finishes the code for Explore as a separate effect).
2017-08-29 05:46:08 +00:00
Sol
b6107f79dd - Fix Xenagos not having it's Legendary PW applied properly 2017-08-29 02:19:25 +00:00
Agetian
fb0acb6a8b - [XLN] Jace, Cunning Castaway: the clones should be immediately activatable as soon as they hit the battlefield. 2017-08-28 19:26:54 +00:00
Agetian
240a898fe5 - Added a TODO entry for Emperor's Vanguard (needs Explore to be updated for the double clause). 2017-08-28 18:49:59 +00:00
Agetian
3aad5916f5 - [XLN] Added Bishop of Rebirth and Walk the Plank. 2017-08-28 18:44:50 +00:00
Agetian
c2a7f19d38 - [XLN] Added Jace, Cunning Castaway. 2017-08-28 18:35:31 +00:00
Agetian
0a5fe2e2af - Some implementation clarifications. 2017-08-28 18:32:05 +00:00
Agetian
f6e5256ae4 - Planeswalker rule: restore the "go to graveyard on 0 loyalty" functionality. 2017-08-28 18:30:25 +00:00
Agetian
46aefe6b8a - [XLN] For now, commenting out the Planeswalker Rule code since it won't apply post-Ixalan. 2017-08-28 17:52:17 +00:00
Agetian
af7ef111b4 - [XLN] Planeswalker uniqueness no longer matters ( http://magic.wizards.com/en/articles/archive/feature/ixalan-mechanics ), planeswalkers are Legendary instead. 2017-08-28 17:39:45 +00:00
Agetian
c918743187 - [XLN] Fixed Admiral Becket Brass. 2017-08-28 17:37:58 +00:00
Agetian
817a691248 - [XLN] Integrating Oracle updates by Indigo Dragon: Planeswalkers now have the Legendary supertype. 2017-08-28 17:35:53 +00:00
Agetian
04e33448e0 - [XLN] Added Admiral Becket Brass. 2017-08-28 17:30:17 +00:00
Agetian
aa4624e344 - [XLN] fixed a card file name. 2017-08-28 15:52:52 +00:00
Agetian
1b7a819a2b - [XLN] fixed a card file name. 2017-08-28 15:52:15 +00:00
Agetian
096036f2a4 - [XLN] Added 10 cards. 2017-08-28 15:50:49 +00:00
Agetian
b488ff053a - Fixed RevealEffect crashing for cards like Vizkopa Confessor. 2017-08-28 15:21:45 +00:00
Agetian
04f24faf32 - Experimental: attempting to fix card flickering (power/toughness, jumping between rows) by selectively updating the view for P/T and types after all static effects for all cards have been processed instead of aggressively updating the entire state view during each static effect operation (might also make the game run a bit faster on mobile).
- Volrath's Shapeshifter: do not copy the target's set code and rarity.
2017-08-28 14:06:28 +00:00
kevlahnota
c180343853 Remove redundant import statement, also refactor addMissingItems method. -kev 2017-08-28 09:44:18 +00:00
kevlahnota
da0739ab21 Refactored decoding URL. This is much better than using regex/replace method. -kev 2017-08-28 09:23:36 +00:00
kevlahnota
aa1d2a1879 Fixed filenames that contains comma and apostrophe when parsed (as seen on achievement resource when downloaded, "%27s" will be replaced by "'s" and %2C" will be replaced by ","), so they can be viewable on achievement page. 2017-08-28 07:54:15 +00:00
Agetian
9d9ef0069a - Adding an empty edition file for the upcoming set Ixalan. 2017-08-28 06:22:57 +00:00
Agetian
50790b94a3 - Fixed Herald Horn. 2017-08-28 05:50:35 +00:00
Agetian
59b7f9c775 - Somewhat better AI for Exhaustion. 2017-08-28 04:25:19 +00:00
Agetian
0f83f23052 - Preparing Forge for Android v1.6.2.004 [hotfix/incremental]. 2017-08-27 19:03:15 +00:00
Agetian
1da95433ae - Fixed the AI sacrificing everything to Westvale Abbey without honoring AIPreference:SacCost (should also fix other similar cards with sac cost that requires the AI to sacrifice several cards). 2017-08-27 19:00:23 +00:00
Agetian
d0579b5f75 - ChangeCombatantsEffect: attempt to account for the new rule for Ulamog, the Ceaseless Hunger + Portal Mage interaction in multiplayer games. Currently achieved by rigging the triggering info on the stack instance, which may not be optimal. Feel free to propose a better solution. 2017-08-27 18:17:21 +00:00
Agetian
1d19f56de8 - Experimental: do not reset the paid hash in resetOnceResolved since it appears to be cleared in other SA operations when needed, and aggressively clearing it breaks transient payment info (e.g. Orator of Ojutai + revealed Dragon card). 2017-08-27 18:15:19 +00:00
Agetian
3688e13137 - Add Basic Lands dialog: make it show only the sets with actual basic lands in them.
- Minor cleanup in addCardToZone in PlayerControllerHuman.
2017-08-27 18:12:32 +00:00
Agetian
45d71d0c5f - AI improvements: restored playability of Exhaustion, made Cursed Scroll AI-playable, added some additional code support for programmable Exert logic (Ahn-Crop Champion is an example), added a bit of threshold to Living Death AI, moved Momir Vig Avatar logic to SpecialCardAi. 2017-08-27 18:11:06 +00:00
Agetian
d8be84b617 - Added quest world "The Gates of Magic" by Seravy. 2017-08-27 18:07:15 +00:00
Agetian
f7c4fe8e91 - Fixed an issue with launching Forge on mobile for some users. 2017-08-27 17:59:15 +00:00
Goblin Hero
9005598f90 Revert previous commit 2017-08-27 17:53:47 +00:00
Goblin Hero
5e508770ef Readme added for server testing purposes 2017-08-27 17:32:46 +00:00
Agetian
8331f92775 - GameState: added support for counters in exile zone (e.g. via Mairsil) and for face-down cards in exile (e.g. via Bomat Courier). 2017-08-27 04:12:19 +00:00
Agetian
4c3c7e3807 - GameState: use setCounters to clear the player/planeswalker counter count 2017-08-27 03:57:40 +00:00
Agetian
1e39e3e815 - Preparing Forge for Android publish 1.6.2.003 [hotfix]. 2017-08-27 03:15:58 +00:00
Agetian
5f6519d4c6 - Fixed a crash in mobile Forge when pausing application. 2017-08-27 03:15:15 +00:00
Agetian
e93c35bcae - Added 3 puzzles coded by Xitax. 2017-08-27 03:14:51 +00:00
Agetian
a9d522d1d9 - Added 2 puzzles coded by Xitax. 2017-08-26 19:34:03 +00:00
Agetian
44ac42a4b5 - Puzzle Mode: A better solution for precasting Awaken in GameState (allows both Awaken and AwakenOnly). 2017-08-26 19:08:35 +00:00
Agetian
78a60c750a - GameState: when processing a precast effect solely for KW Awaken, only precast the Awaken part, not the parent ability. 2017-08-26 18:54:39 +00:00
Agetian
6012b06a38 - Added 3 puzzles coded by Xitax. 2017-08-26 18:39:50 +00:00
Agetian
3ff370a903 - Fixed a missing reference in Magus of the Mind (matters for Mairsil, the Pretender - fixes a crash when activating a Mairsil'ed copy of the Magus's ability). 2017-08-26 18:30:06 +00:00
Agetian
6147d64a74 - Preparing Forge for Android publish 1.6.2.002. 2017-08-26 18:19:14 +00:00
Agetian
91b839dd26 - Fixed a NPE in ComputerUtil. 2017-08-26 18:17:31 +00:00
Agetian
cdb6d10519 - Genju of the X cycle of cards AI: attempt to avoid tapping the targeted card for mana if there's another source like that available (might be further improved later by detecting the actual mana type and the sources of that type that are open, after which the special AI parameter SVar might no longer be necessary). 2017-08-26 18:01:03 +00:00
Agetian
586abb5466 - Use updateAbilityTextForView as a limited form of updating the text part of the view in order to update changed card text (e.g. lands affected by Blood Moon, creatures affected by Humility) while avoiding flickering side effects.
- Volrath's Shapeshifter: fixed a concurrent modification when removing temporary triggers.
2017-08-26 17:12:08 +00:00
Agetian
710ac14202 - Fixed Neko-Te. 2017-08-26 17:01:46 +00:00
Agetian
a8735d1b4c - Restored a separator space in SettingsPage. 2017-08-26 15:15:07 +00:00
Agetian
f9ede752f2 - Added a new option to mobile Forge which might help deal with the texture issues after locking/unlocking the screen (suggested by kevlahnota). 2017-08-26 15:13:46 +00:00
Agetian
5bdb205168 - Mobile Forge: new game installations enable battlefield texture filtering by default. 2017-08-26 14:58:01 +00:00
Agetian
b606b939a6 - Fixed AI logic name in Oracle's Vault. 2017-08-26 14:53:59 +00:00
Agetian
0e0b12dcc0 - Fixed keyword name capitalization in AiAttackController. 2017-08-26 14:49:03 +00:00
Agetian
b9e7b1f29b - Attack AI: Afflict should be considered as a combat effect for the purpose of attacking into non-lethal blockers. 2017-08-26 14:46:50 +00:00
Agetian
f930f2aa80 - Minor comment clarification. 2017-08-26 06:22:15 +00:00
Agetian
8698f7e4ab - Bomat Courier AI improvement: teach the AI to sac Courier when threatened in case sacrificing it would provide hand card advantage 2017-08-26 06:17:56 +00:00
Agetian
40b62c09f7 - Fixed references in Soul Burn and Drain Life.
- Promoted Wildfire and Burning of Xinye from RemAIDeck to RemRandomDeck.
2017-08-26 04:14:27 +00:00
Agetian
8ddc3ae172 - Fixed Jeskai Charm. 2017-08-25 20:27:26 +00:00
Agetian
b3e2fb4046 - Documenting changes in CHANGES.txt. 2017-08-25 19:28:43 +00:00
Agetian
42558f2bd4 - A better, theme-oriented planeswalker attacker targeting arrows with the default orange-ish color.
- Removed the unused "darker PW arrows" option.
2017-08-25 19:26:13 +00:00
Agetian
1e23b0a17e - Documenting changes in CHANGES.txt. 2017-08-25 17:12:35 +00:00
Agetian
5b6e0d5c10 - CounterAi: a more generic solution for Mental Misstep exclusion, part 2 2017-08-25 15:55:28 +00:00
Agetian
a53de75873 - CounterAi: a more generic solution for counterspells only targeting CMC X vs. CMC X spells. 2017-08-25 15:52:52 +00:00
Agetian
b3dc4671d0 - Fixed the attack AI overevaluating board position because unblocked attackers with evasion were not removed from remainingAttackers before further evaluation. 2017-08-25 15:35:29 +00:00
Agetian
ef4dd57032 - NPE prevention in ComputerUtil. 2017-08-25 14:59:22 +00:00
Agetian
ad1e17f329 - Fixed a NPE in ComputerUtilCost, part 2. 2017-08-25 11:01:48 +00:00
Agetian
2ba53e2dab - Fixed a NPE in ComputerUtilCost. 2017-08-25 10:59:29 +00:00
Agetian
909b8e7127 - Bristling Hydra AI: teach the AI to use a ping activation to save the hydra from death. 2017-08-25 10:57:12 +00:00
Agetian
8280401e10 - A better algorithm for detecting available mana for the purpose of CounterAi. Should be more precise and less cheat-y. 2017-08-25 09:21:02 +00:00
Agetian
6096fc1c92 - Documenting changes in CHANGES.txt. 2017-08-25 06:10:14 +00:00
Agetian
0378d54acc - Added implementation comment. 2017-08-25 05:51:54 +00:00
Agetian
d33b642b8e - A more universal CounterAi fix, accounting for sources producing multiple mana. 2017-08-25 05:49:00 +00:00
Agetian
27d68bcf45 - Fixed CounterAi in the trigger portion as well. 2017-08-25 05:28:06 +00:00
Agetian
b0122a8e38 - Renamed a method to be more self-explanatory. 2017-08-25 05:26:50 +00:00
Agetian
96cbb63c84 - CounterAi: fixed the AI not accounting for the mana in the mana pool of the SA activator; fixed the AI looking at the mana of the wrong opponent in multiplayer matches. 2017-08-25 05:23:27 +00:00
Agetian
bd16899c56 - Reorganized the dev mode panel to make a bit more sense (grouped the buttons by function, more or less).
- Added "Repeat Last Add Card" functionality to dev mode panel.
2017-08-25 04:53:31 +00:00
Agetian
b874a2b76d - Added a fix by Seravy to prevent the large quest shop pools from hanging the game because statistics were updated for them during selling. 2017-08-25 04:11:14 +00:00
Agetian
aaf16273a9 - Minor code style unification. 2017-08-24 17:46:01 +00:00
Agetian
f83e03e775 - Fixed a comment. 2017-08-24 17:45:01 +00:00
Agetian
5328e443db - Fixed the AI tapping a creature for mana to cast a pump instant/sorcery spell on that creature. 2017-08-24 17:44:03 +00:00
Agetian
ed49943039 - Further improvements to AI handling Electrostatic Pummeler. 2017-08-24 17:27:14 +00:00
Agetian
220fdcdc75 - Added an overriding implementation of doTriggerAINoCost to BondAi, fixes the AI randomly ignoring a chance to soulbond creatures. 2017-08-24 16:21:23 +00:00
Agetian
a1b2a5149d - Volrath's Shapeshifter: run a limited update for ability and keyword text such that the changed text is always visualized (e.g. things like Rally triggers). 2017-08-24 15:09:18 +00:00
Agetian
3090a991a6 - Fixed Scalelord Reckoner triggering for cards not on the battlefield. 2017-08-24 13:31:41 +00:00
Agetian
e0cae27f2f - Reverting an inadvertent change. 2017-08-24 13:30:58 +00:00
Agetian
21c7c74b31 - Attempting to fix Dragon Presence cards, part 1: "dragon presence" apparently cares about the current battlefield state, not the LKI (from Gatherer: "you must control a Dragon as you are finished casting the spell to get the bonus. For example, if you lose control of your only Dragon while casting the spell (because, for example, you sacrificed it to activate a mana ability), you won’t get the bonus"). Also fixes Orator Ojutai not drawing a card at all when a Dragon is present on the battlefield. 2017-08-24 13:29:46 +00:00
Agetian
441693ac36 - Added a couple break statements. 2017-08-24 11:49:08 +00:00
Agetian
77910d82fc - Improved AI spending counters for Skullmane Baku. 2017-08-24 11:47:59 +00:00
Agetian
05258a5b48 - Some improvement to the AI for "remove X counters": made the Baku cards that are marked as RemRandomDeck (but not RemAIDeck) playable by the AI again, albeit some of them rather suboptimally. Quillmane Baku is not supported (but was already marked RemAIDeck). 2017-08-24 10:28:43 +00:00
Agetian
387abd1606 - Clarification for the new setting. 2017-08-24 05:51:16 +00:00
Agetian
3e4efc22a6 - Added an option to make planeswalker attacker targeting arrows somewhat darker to make them easier to see on the battlefield.
- Added code support to differentiate stack targeting arrows in color as well (currently not used).
2017-08-24 05:48:17 +00:00
Agetian
2851e51af8 - Fixed Izzet Chemister not having Haste. 2017-08-23 19:38:40 +00:00
Agetian
ad8ad35c03 - One more fix. 2017-08-23 19:37:21 +00:00
Agetian
20d27bb9b3 - Removed an unused parameter. 2017-08-23 19:37:00 +00:00
Agetian
30e878cbf9 - Fixed the previous commit. 2017-08-23 19:36:29 +00:00
Agetian
374f6fcc1b - Improvements to the Electrostatic Pummeler AI logic. 2017-08-23 19:35:45 +00:00
Agetian
4ea7d68bff - Oviya Pashiri event in Kaladesh: added missing lands. 2017-08-23 17:32:41 +00:00
Agetian
88491f61e2 - Some more improvements for Electrostatic Pummeler AI 2017-08-23 17:20:33 +00:00
Agetian
abe710f3ab - Some more fine-grained control over AILogic PayEnergyConservatively. 2017-08-23 15:16:47 +00:00
Agetian
3e4e87db23 - GameState: clear counters on planeswalkers and players before applying new ones from the game state. 2017-08-23 15:01:04 +00:00
Agetian
e7ca1a13a6 - Renamed a method. 2017-08-23 14:47:54 +00:00
Agetian
aca78410f6 - Moved a comment. 2017-08-23 14:47:24 +00:00
Agetian
989331cbb4 - Some improvements for Pump AI related to pump spells with pure energy cost, makes Electrostatic Pummeler a little better in AI's hands. 2017-08-23 14:45:01 +00:00
Agetian
7a8267a772 - A less reckless default AILogic PayEnergyConservatively for Longtusk Cub and Bristling Hydra (gives the AI a chance to use the energy for something else when possible). 2017-08-23 12:53:44 +00:00
Agetian
37ca383360 - Formatting code in DamageAllAi. 2017-08-23 08:52:51 +00:00
Agetian
90eb55fbf8 - Made DamageAllAi more multiplayer friendly + improved the logic for seeing if both a player can be killed and some more creatures can be dealt with in multiplayer environment.
- Marking ComputerUtil.getOpponentFor as deprecated and adding a comment about what it should be replaced with over time. Also, simplified its implementation since at the moment it's a functional synonym of getWeakestOpponent.
2017-08-23 08:51:28 +00:00
Agetian
e96c503b11 - DamageAllAi: prioritize killing the player off, if possible (e.g. Earthquake for X=1 to kill creatures or X=3 to kill a player). 2017-08-23 04:58:29 +00:00
Agetian
99ce6cf29d - isValid: A player who lost the game leaves it and can't be a valid target of spells or abilities. 2017-08-22 06:59:59 +00:00
Agetian
e14952607d - Fixed Kilnspire District. 2017-08-22 06:55:17 +00:00
Agetian
de3efa0fe7 - Fixed a typo. 2017-08-22 06:09:50 +00:00
Agetian
9af123523a - Further improvement to Wand of Ith prompt. 2017-08-22 06:09:31 +00:00
Agetian
7bc5adbde2 - Fixed an extra character at the EOL in Wand of Ith. 2017-08-22 06:08:32 +00:00
Agetian
5287978e2f - Fixed Wand of Ith triggering the opponent's Sangromancer when the Wand's controller forces that opponent to discard a card. 2017-08-22 06:04:46 +00:00
Agetian
d364cc0f51 - Improved the experimental attack evasion prediction by considering creatures with evasion first (further tweaking and testing in progress). 2017-08-22 05:10:11 +00:00
Agetian
e7c30da23c - Improved prompt for Wand of Ith. 2017-08-22 04:33:56 +00:00
Agetian
1e6467cf80 - Experimental: attempting to improve the AI choice for attrition attack when predicting possible opponent's forces with evasion (e.g. Flying). Currently enabled only for the Experimental AI profile for the testing period. 2017-08-21 17:29:57 +00:00
Agetian
12452c9b2a - Fixed the AI profile files. 2017-08-21 17:23:10 +00:00
Agetian
92b985d24d - Experimental: attempting to improve the AI choice for all-in assault for battlefield situations where the defender will have several, but not enough, defenders with evasion (e.g. Flying).
- Currently only enabled for the Experimental AI profile for the testing period.
2017-08-21 17:07:46 +00:00
Agetian
a0f640c739 - Some code maintenance. 2017-08-21 16:10:48 +00:00
Agetian
46ecbc9c42 - Dev Mode: added a new function "Remove Card from Game", which allows to completely remove a card from the game in case it was added previously by mistake. 2017-08-21 16:09:20 +00:00
Agetian
96f97e41e1 - Create a Puzzle: allow to choose to start the game either with the Human or with the AI player taking the first turn.
- Improved the opening warning message for this mode.
2017-08-21 15:52:57 +00:00
Agetian
5726f05531 - Comment fix. 2017-08-21 15:19:00 +00:00
Agetian
b389209c05 - Added a simple Create Puzzle mode to desktop Forge (presents you with a clean battlefield, allows to dump the game state with a template for puzzle metadata).
- Enhanced the Dev Mode feature set with the "Add Card To Library/Graveyard/Exile" commands.
- Ported this Dev Mode functionality, as well as "Exile Card From Hand/Play", to mobile Forge.
2017-08-21 15:17:41 +00:00
Agetian
a82b3fd88a - Removed debug print lines. 2017-08-21 11:25:30 +00:00
Agetian
04a125d20f - A somewhat more conservative and fine-grained approach at improving the token-generation AI logic. 2017-08-21 11:24:47 +00:00
Agetian
fc2e93a57f - Converted token generation ability activation chance for the AI into a profile variable (currently defaults to 100% for all profiles, pending testing). 2017-08-21 10:29:30 +00:00
Agetian
cfe9c17b58 - Experimental: TokenAi: do not use a "80% chance to generate a token" chance when the token-generating ability is otherwise relevant. Prevents the AI from missing activations of planeswalker token generation abilities on casting a planeswalker, as well as indecisively creating tokens via The Hive and other similar cards before Declare Blockers when on defense. Will run some tests with this later, or may convert to an AI profile variable. 2017-08-21 10:19:48 +00:00
Agetian
b24f31f98c - Fixed an issue with the new quest card price format and cards with multiple art index (e.g. Arcane Denial from ALL or basic lands). 2017-08-21 10:04:36 +00:00
Agetian
5263edc3e2 - Minor restructuring/update in Puzzle code. 2017-08-21 09:44:45 +00:00
Agetian
99b80cccad - Unified puzzle description for INQ01. 2017-08-21 06:50:18 +00:00
Agetian
9794c5a188 - Added puzzle INQ03 (Dead Man's Hand #03). 2017-08-21 06:46:36 +00:00
Agetian
2bd19736f4 - Added puzzle INQ02 (Dead Man's Hand #02). 2017-08-21 06:06:55 +00:00
Agetian
13376d0ced - Improved handling of script execution in GameState to support subabilities and KW Awaken (other keywords with mana cost might need similar treatment later). 2017-08-21 04:50:49 +00:00
Agetian
1aa8475295 - Added a special turn ID correction for puzzles that begin right before the beginning of the human's turn (e.g. INQ01).
- Added support for "Gain Control of Specified Permanents" goal type to Puzzle Mode.
2017-08-20 18:55:12 +00:00
Agetian
4bffe22f68 - Added puzzle INQ01 (Inquest Gamer - Dead Man's Hand #01). 2017-08-20 18:36:58 +00:00
Agetian
094941ab8c - Added support for Imprinted cards to GameState. 2017-08-20 17:51:29 +00:00
Agetian
1a1bcc8d5c - Added support for "Play the Specified Permanent" type objective to Puzzle Mode (e.g. Inquest puzzles). 2017-08-20 17:37:54 +00:00
Agetian
e77eb8a563 - Added a new C17 card to Kaladesh plane in Planar Conquest. 2017-08-20 16:17:17 +00:00
swordshine
c063ccfa02 - C17: Added Portal Mage 2017-08-20 15:31:20 +00:00
Agetian
b1ccb5c3de - Fixed a couple weirdly auto-formulated stack/prompt descriptions. 2017-08-20 15:07:11 +00:00
Agetian
29b4704c4b - Reverted adding Shining Shoal (misread how the card works, need prevention from source, will see if I can update the script later). 2017-08-20 11:59:26 +00:00
Agetian
931d7d55dd - Fixed description for Shining Shoal. 2017-08-20 11:45:52 +00:00
Agetian
2df0403c94 - Added Shining Shoal (uses the same scripting strategy as Captain's Maneuver). 2017-08-20 11:45:35 +00:00
Agetian
9cd878bad1 - More Volrath's Shapeshifter QoL. 2017-08-20 05:12:17 +00:00
Agetian
4c816eabc1 - Reverting an accidental experimental test line commit. 2017-08-20 04:24:38 +00:00
Agetian
d28cbf3863 - Some Volrath's Shapeshifter fixes and QoL improvements. 2017-08-20 04:23:11 +00:00
swordshine
6269719b70 - Fixed last commit 2017-08-20 03:28:03 +00:00
swordshine
8a347e71ed - C17: Added Alms Collector 2017-08-20 03:26:18 +00:00
Agetian
9c44662f2e - Fixed a NPE in the new AiController code. 2017-08-20 03:24:25 +00:00
Agetian
884291a8a3 - Fixed a PO2 challenge deck title and description. 2017-08-20 03:23:15 +00:00
Agetian
97428909a4 - Minor code style fix. 2017-08-19 19:38:10 +00:00
Agetian
a0bb52ff5f - Added Volrath's Shapeshifter with rudimentary, simple AI support.
- Was tested in most typical circumstances, including cloning it. However, may not yet be perfect in some corner cases. Improvements are welcome.
2017-08-19 18:05:44 +00:00
Agetian
1a766c3ccc - Fixed Edgar Markov. 2017-08-19 13:06:39 +00:00
Agetian
19bea70dc0 - Added an implementation note. 2017-08-19 09:18:14 +00:00
Agetian
322a7020e3 - Better fix for Aluren: detect zone granting permissions from the MayPlay card options themselves, not by card name.
- Fixed Qasali Ambusher.
2017-08-19 09:15:09 +00:00
Agetian
242444ecc9 - Attempting to fix Aluren. 2017-08-19 07:31:42 +00:00
Agetian
4820ebba84 - Added AILogic$ ExileGraveyards to more cards. 2017-08-19 06:05:09 +00:00
Agetian
7658481db2 - Slightly improved AI for Scavenger Grounds activated ability. 2017-08-19 06:02:34 +00:00
Agetian
f41644bc97 - Fixed Crook of Condemnation mana cost. 2017-08-19 03:41:54 +00:00
Agetian
2163eb5910 - Fixed the AI playability of Bargain. 2017-08-18 19:26:09 +00:00
Agetian
ffbaf1e54f - Added Rock Hydra. 2017-08-18 19:02:16 +00:00
Agetian
76ac47bbc6 - Preparing Forge for Android publish 1.6.2.001 [incremental]. 2017-08-18 17:15:11 +00:00
Blacksmith
d499f1af65 Clear out release files in preparation for next release 2017-08-18 17:12:17 +00:00
Blacksmith
a249eeab22 [maven-release-plugin] prepare for next development iteration 2017-08-18 17:07:15 +00:00
Blacksmith
712e7dbeb8 [maven-release-plugin] prepare release forge-1.6.2 2017-08-18 17:07:05 +00:00
Blacksmith
22571acd0c Update README.txt for release 2017-08-18 17:05:06 +00:00
Agetian
eba8d0bd80 - Adding support for C17 cards to Planar Conquest [part 2]. 2017-08-18 17:03:07 +00:00
Agetian
4b52dea65a - Adding support for C17 cards to Planar Conquest. Some commanders can be moved to other planes later once they're implemented (e.g. Mirri to Dominaria, etc.). 2017-08-18 17:01:41 +00:00
Agetian
a5eeaae0d0 - Prevent a NPE when trying to zoom a card before a single card is selected. 2017-08-18 15:23:08 +00:00
Agetian
0d85eb6b90 - Documenting changes in CHANGES.txt. 2017-08-18 15:20:37 +00:00
Agetian
3abb226b5c - Added a keyboard shortcut to zoom in/out of the card in desktop Forge (default Z). 2017-08-18 14:53:20 +00:00
Agetian
d38fdfb291 - Documenting changes in CHANGES.txt. 2017-08-18 14:35:58 +00:00
Agetian
a63361302d - Migrating C17 from upcoming to named folders. 2017-08-18 14:34:54 +00:00
Agetian
069dbce000 - Removed a P/T assignment from Abandoned Sarcophagus. 2017-08-18 14:32:39 +00:00
Agetian
5970c575c8 - [C17] Added Mirri, Weatherlight Duelist.
- Not sure if oneBlockerPerOpponent is better off as a global rule or a keyword, feel free to change it to a keyworded implementation if appropriate.
2017-08-18 11:32:22 +00:00
Agetian
6ba659ff55 - Fixed the second, third, etc. AI never blocking anything in multiplayer when multiple players are attacked at the same time. 2017-08-18 06:22:55 +00:00
Agetian
2c9f0e3c7d - Fixed the AI incorrectly considering sorcery-speed tap abilities like Outlast, thus never using them. 2017-08-18 06:07:37 +00:00
Agetian
06359b9d06 - A clarification in CHANGES.txt. 2017-08-18 04:00:58 +00:00
Maxmtg
7f31fd5092 Split forge script handlign code 2017-08-17 21:10:06 +00:00
Agetian
01cdd82cc5 - Documenting changes in CHANGES.txt. 2017-08-17 17:13:59 +00:00
Agetian
c6094cf3ee - Documenting changes in CHANGES.txt. 2017-08-17 16:39:55 +00:00
Agetian
2ba7da0486 - Conspiracy: enabling Conspiracy draft sets in block definitions. 2017-08-17 16:33:28 +00:00
Agetian
dcc0295b4b - Conspiracy: adding CN2 rankings. 2017-08-17 16:28:58 +00:00
Agetian
8632b0ffec - Conspiracy: a little restructuring of chooseCardName in PlayerControllerAi. 2017-08-17 15:34:23 +00:00
Agetian
bc0b69857a - Conspiracy: The AI should not spam all the conspiracies with the same card name, instead choosing a new name every time. 2017-08-17 15:31:32 +00:00
Agetian
198d48fd84 - Conspiracy: allow the player to look at his own face-down conspiracies. Properly hide the opponent's conspiracies (including the chosen name). Make the AI properly add drafted conspiracies to the [Conspiracy] section of the deck. 2017-08-17 14:52:36 +00:00
Agetian
e85d6bebd0 - Shuffle the zones after the initVariantZones call. 2017-08-17 12:37:54 +00:00
Agetian
0679d9a30f - Conspiracy: fixed the AI never putting any conspiracies into the command zone. Added some simple logic for the AI revealing conspiracies when a spell with the chosen name was cast during any given turn. 2017-08-17 12:31:27 +00:00
Agetian
b0584d4499 - Fixed Hidden Agenda conspiracy cards crashing the game and their reveal ability not working. 2017-08-17 11:50:21 +00:00
Agetian
51bfc37a27 - Updated description to puzzle PS_AKH4. 2017-08-17 07:01:55 +00:00
Agetian
c979b27653 - Added description to puzzle PS_AKH4. 2017-08-17 07:01:26 +00:00
Agetian
184f0fad99 - Added puzzle PS_AKH7 coded by Rooger. 2017-08-17 06:38:14 +00:00
Agetian
f9b88c2738 - Removed a comment that doesn't apply anymore. 2017-08-17 06:35:38 +00:00
Agetian
f47a679dda - Allow precasting specific SAs from cards by their ordinal number (allows e.g. precasting planeswalker abilities).
- Added puzzle PS_AKH0 scripted by Rooger.
2017-08-17 06:34:55 +00:00
Agetian
e5e56b5260 - Allow precasting specific SAs from cards when setting up game state (needed by Puzzle Mode game states). 2017-08-17 06:06:44 +00:00
Agetian
c62f6b41d2 - A more correct interpretation of 901.6+901.10: the next player in turn order should initPlane in a planechase game if the planar controller leaves the game. 2017-08-17 05:37:33 +00:00
Agetian
6b32c4997d - Fixed Selesnya Loft Gardens. 2017-08-17 04:56:27 +00:00
Agetian
cb89c96dac - Momir Basic: when auto-generating the deck, only update the original deck instead of fully overwriting it. Allows the Momir Basic variant to be used in conjunction with other variants, such as Planechase, without crashing Forge. 2017-08-17 04:51:43 +00:00
Agetian
b31ca263fd - Implemented rule 901.6 for Planechase games. Should fix an issue with the planar deck cards disappearing when a player loses the game on his or her own turn. 2017-08-17 04:13:32 +00:00
Agetian
a0451cfab0 - Added 2 puzzles coded by Xitax. 2017-08-17 03:45:11 +00:00
Maxmtg
4baecd9f79 hasProperty of SpellAbility moved to ForgeScript 2017-08-16 21:38:26 +00:00
Maxmtg
381851aba7 moved player's hasProperty to ForgeScript 2017-08-16 20:36:30 +00:00
Maxmtg
23d3d5973b Move hasProperty of Card to ForgeScript 2017-08-16 20:10:26 +00:00
Agetian
fcdf9cf2ec - Do not run the replacement handler query if defender damage is determined to be 0. 2017-08-16 13:34:49 +00:00
Agetian
9831236d79 - Combat AI: Query the replacement handler for possible damage prevention when trying to determine if a blocker can destroy the attacker (fixes e.g. the AI chump blocking an exerted Oketra's Avenger even though all damage is to be prevented). 2017-08-16 13:32:30 +00:00
Agetian
c644d6bf2f - Removed a duplicated comment. 2017-08-16 12:26:41 +00:00
Agetian
d82b008c33 - Removed a testing useless reference. 2017-08-16 12:25:58 +00:00
Agetian
d53f9e99af - Fixed a miscalculation and related NPE when trying to predict the bonus from Arahbo, Roar of the World.
- Some extra NPE prevention measures in related trigger processing code.
2017-08-16 12:25:23 +00:00
Agetian
870b168cea - Disposal Mummy trigger is not optional. 2017-08-16 08:54:25 +00:00
Agetian
094e2478f9 - C17 rule update: tokens can phase out and phase in now (August 11, 2017 release notes). 2017-08-16 06:40:06 +00:00
Agetian
110b9f5058 - Removing some extra spaces in Commander 2017 edition file. 2017-08-16 06:10:40 +00:00
Agetian
d959753641 - A more generic check for LKI in ChangesZone Battlefield->Graveyard triggers (e.g. Dross Scorpion). 2017-08-16 06:08:46 +00:00
Agetian
027ecf29ae - Experimental: check LKI for the ChangesZone trigger of Dross Scorpion (fixes interaction with cards equipped with cards providing the Artifact type, e.g. Firemaw Kavu + Silversilk Armor). Should this be the default behavior? 2017-08-16 05:08:56 +00:00
Agetian
d4392635bf - Added 2 puzzles coded by Xitax. 2017-08-16 04:53:38 +00:00
Agetian
c72370eff0 - Removed an unused line from a test. 2017-08-16 03:48:18 +00:00
Agetian
6b799be7dd - Added support for PhasedOut to GameState. 2017-08-16 03:47:54 +00:00
Maxmtg
244d5bba47 Moved hasProperty code (it's forge script parsing in fact) out of CardState object to keep the class clean. 2017-08-15 21:40:03 +00:00
Agetian
d539ddf018 - Documenting changes in CHANGES.txt. 2017-08-15 19:28:25 +00:00
Agetian
f279792273 - AI should not overprioritize Gavony Township, otherwise it locks itself on mana too much. 2017-08-15 19:19:10 +00:00
Agetian
aa4a793566 - Added AILogic$ Fight to Ambuscade. 2017-08-15 17:41:20 +00:00
Agetian
1aaa2340f8 - Menace: do not display reminder text in game (an evergreen keyword that has appeared with no reminder text for a while). 2017-08-15 16:29:41 +00:00
Agetian
329247391b - Integrating Oracle updates by Indigo Dragon. 2017-08-15 16:10:51 +00:00
Agetian
3fc5daef01 - Corrected descriptions for Embalm and Eternalize. 2017-08-15 16:07:41 +00:00
Agetian
e087d04c17 - Basic AI logic for Shadow of the Grave. 2017-08-15 14:45:53 +00:00
Agetian
a3a713b608 - Preparing Forge for Android publish 1.6.1.005 [incremental]. 2017-08-15 13:45:45 +00:00
Agetian
98251a8631 - Added puzzle PC_090115 coded by Xitax. 2017-08-15 13:40:16 +00:00
Agetian
b147a16487 - Simplified implementation of LastRemembered. 2017-08-15 11:06:54 +00:00
Agetian
411b86197a - RememberedLast->LastRemembered for consistency with FirstRemembered (+ move the code closer to FirstRemembered). 2017-08-15 11:05:25 +00:00
Agetian
8acab6b662 - Minor formatting fix. 2017-08-15 11:00:21 +00:00
Agetian
dba550550c - Shifting Shadow: use the last remembered object as a reattachment target for compatibility with cards that remember other things too (e.g. Myr Welder). 2017-08-15 10:59:48 +00:00
Agetian
74cfc733dd - Removed code from the previous implementation of Shifting Shadow which is no longer needed. 2017-08-15 10:35:40 +00:00
Agetian
23981accd1 - A cleaner and hopefully sturdier implementation of Shifting Shadow that avoids the need for an intermediate effect card. 2017-08-15 10:32:37 +00:00
Agetian
209f34124e - Fixed Grasp of the Hieromancer. 2017-08-15 07:14:11 +00:00
Agetian
14cff4facf - Fixed Kothophed, Soul Hoarder trigger description. 2017-08-15 07:08:10 +00:00
Agetian
dac089b0fb - Added puzzle PC_082515 coded by Xitax. 2017-08-15 06:54:24 +00:00
Agetian
52aeda7faf - Avoid using a dedicated special parameter for Shifting Shadow spell description override. 2017-08-15 06:49:10 +00:00
Agetian
5273f5c9b6 - NPE protection measures. 2017-08-15 06:40:18 +00:00
Agetian
d2a30ea049 - [C17] Added Shifting Shadow.
- One of the hackier/more complicated scripts, so may not be perfect, though was tested in most situations, including stealing the enchanted creature.
2017-08-15 06:35:08 +00:00
Agetian
716ba3eea8 - Fixed a miscalculation in DamageAllAi. 2017-08-15 04:42:59 +00:00
Agetian
5cece53e50 - Fixed Madcap Experiment and Heirloom Blade "...the rest on the bottom of your library in a random order" clause. 2017-08-14 14:53:24 +00:00
Agetian
ba73f8a323 - ChangeZoneAi: do not activate the AF if it'll try to put a creature on the battlefield that will end up with toughness below zero after it enters the battlefield (Reassembling Skeleton + Elesh Norn, Grand Cenobite). 2017-08-14 14:13:14 +00:00
Agetian
81379a49a7 - A better variable name for Magus of the Mind. 2017-08-14 11:14:07 +00:00
Agetian
77e4862b3e - Minor formatting fix. 2017-08-14 11:01:59 +00:00
Agetian
1a3bd2aa74 - [C17] Added Magus of the Mind. 2017-08-14 11:01:36 +00:00
Agetian
9997b4a2de - A somewhat more comprehensive anti-cheating solution for Cavern of Souls AI. 2017-08-14 05:15:30 +00:00
Agetian
74b12606ef - Prevent the AI from cheating with Cavern of Souls, paying an arbitrary amount of mana with it. 2017-08-14 04:50:22 +00:00
Hanmac
14d5034b8e make Worms of the Earth into a GlobalRuleChange 2017-08-13 18:35:10 +00:00
Agetian
8b282818e9 - Fixed a couple NPEs in mobile Forge when canceling a Sealed/Draft match with a specific opponent. 2017-08-13 15:47:27 +00:00
Agetian
48a4f8ba67 - [C17] Removed the TODO entries from Curses since they use RepeatEach now, which should theoretically work for multiple attackers when that is implemented. 2017-08-13 15:18:59 +00:00
Agetian
1dd048eed4 - A little update to the previous commit. 2017-08-13 15:15:18 +00:00
Agetian
e398d6af75 - [C17] updated Curses to work correctly for "you and the triggered attacking player". 2017-08-13 15:13:32 +00:00
Agetian
39b1c1f7e4 - Removed a couple empty lines in new scripts. 2017-08-13 14:21:37 +00:00
Agetian
ff78b384ac - [C17] Added Izzet Chemister. 2017-08-13 14:10:35 +00:00
Agetian
d45a6f94f5 - [C17] Added Galecaster Colossues, Kindred Boon, Kindred Dominance. 2017-08-13 10:37:50 +00:00
Agetian
48fe8d5762 - Fixed a mistype in the C17 edition definition file. 2017-08-13 10:10:45 +00:00
Agetian
36b493f03a - Puzzle Mode: don't award Challenge achievements in this mode (take two). 2017-08-13 09:46:51 +00:00
Agetian
670ccaf891 - [C17] Added the 5 Curse cards. Currently there is no way to ensure that they work correctly with multiple attackers since Forge doesn't support shared turns yet. If support for shared turns is introduced, this should work correctly if TriggeredAttackingPlayer is expanded to return multiple attackers or if a RepeatEach construct is introduced iterating over an array of all attackers. 2017-08-13 09:40:18 +00:00
Maxmtg
d16a48a1a2 Move booster generation code to a separate package 2017-08-13 09:01:45 +00:00
Maxmtg
3f4eedbeab clean up more warnings about unused fields 2017-08-13 08:29:54 +00:00
Maxmtg
ef3dd4a833 Remove unused imports - to decrease number of warning from IDE. 2017-08-13 08:26:42 +00:00
Agetian
fee074db42 - Added a basic NeedsToPlayVar to other Pacts. 2017-08-13 07:19:57 +00:00
Agetian
5463af7f60 - Added a basic NeedsToPlayVar to Pact of the Titan. 2017-08-13 07:17:03 +00:00
Agetian
b776cd4a91 - Added a basic NeedsToPlayVar to Pact of Negation. 2017-08-13 03:55:33 +00:00
Maxmtg
45945e839f fix more warnings 2017-08-13 02:25:48 +00:00
Maxmtg
043ad7e3aa simplify ConquestAwardPool 2017-08-13 02:19:15 +00:00
Maxmtg
d04e186dea more unused methods and imports removal 2017-08-13 01:44:36 +00:00
Maxmtg
4253365e03 Moved AIOption to ai package, where it belongs 2017-08-13 00:53:26 +00:00
Maxmtg
07437b7880 Clean up unused imports that popped up in eclipse warnings List 2017-08-13 00:40:48 +00:00
Maxmtg
5ddd007f67 Remove player.getOpponent method, route former calls from AI through ComputerUtil.getOpponentFor(player) 2017-08-13 00:27:26 +00:00
Agetian
814978a178 - Fixed a NPE related to one of the previous commits. 2017-08-12 16:15:30 +00:00
Agetian
5350e77125 - When a permanent leaves the battlefield, remove all changed keywords on it (fixes e.g. the results of Magical Hack on Leviathan still partially persisting after it dies and is reanimated by something). 2017-08-12 14:56:22 +00:00
Agetian
22e2e32377 - Some subtype corrections. 2017-08-12 14:13:29 +00:00
Agetian
58b2c36498 - Added a comprehensive map of plural card subtypes to their singular counterparts, to the best of my knowledge of how the plural forms are formed (please take a look and update if necessary). Needed for the correct function of text change effects with any type (e.g. Homing Sliver + New Blood).
- Added an exception for the card text generation for the compound subtype "Eldrazi Scion" (e.g. Kor Castigator).
2017-08-12 13:58:26 +00:00
Agetian
0e4af7dbcf - Kess, Dissident Mage: don't remember the instant/sorcery card in case it was bounced from stack to a zone other than graveyard (e.g. to hand via Remand). 2017-08-12 12:37:52 +00:00
Agetian
3aba1c5ccf - Added NeedsToPlay AI var to Vizier of Many Faces. 2017-08-12 10:56:51 +00:00
Agetian
44af80f336 - [C17] Added O-Kagachi, Vengeful Kami. 2017-08-12 09:53:55 +00:00
Agetian
4591f5b372 - Updated Commander 2017 edition file. 2017-08-12 09:30:22 +00:00
Agetian
2270b2a224 - Mairsil AI: do not cage the same card twice. 2017-08-12 05:42:07 +00:00
Agetian
4eb68aae77 - [C17] Added Kess, Dissident Mage.
- May not be perfect in some corner cases (not sure), feel free to improve.
2017-08-12 05:34:16 +00:00
Agetian
c92e3cca6b - Fixed Watchers of the Dead exiling everything from its activator's graveyard. 2017-08-12 04:22:40 +00:00
Agetian
13ba0121d7 - Use isEmpty() to test an empty string. 2017-08-11 17:31:46 +00:00
Agetian
be4943a9d6 - Added rudimentary AI logic for Mairsil, the Pretender. 2017-08-11 17:23:58 +00:00
Agetian
9484e3b52d - Initial Commander 2017 edition file by Indigo Dragon. 2017-08-11 16:51:07 +00:00
Agetian
ef731703e8 - [C17] Added Fortunate Few. 2017-08-11 16:49:19 +00:00
Agetian
11b86806de - [C17] Added Taigam, Sidisi's Hand. 2017-08-11 15:35:24 +00:00
Agetian
b872f59a01 - Improved text for Path of Ancestry. 2017-08-11 14:24:01 +00:00
Agetian
b2fc1cc1f2 - [C17] Added Path of Ancestry.
- Fixed Command Tower and Path of Ancestry generating 2 colorless mana in non-EDH matches.
2017-08-11 14:14:57 +00:00
Agetian
6907c9c550 - Keeping num final in CharmEffect. 2017-08-11 09:46:13 +00:00
Agetian
93701585f8 - Somewhat improved Vindictive Lich (still doesn't play ball with hexproofed opponents though). 2017-08-11 09:40:59 +00:00
Agetian
50e596e63a - Mairsil, the Pretender: made the activations per turn limit parameter more generic. 2017-08-11 08:24:09 +00:00
Agetian
51ece30c34 - [C17] added Vindictive Lich.
- Might need an additional CharmEffect update in case the fact that you can choose multiple modes even with one opponent (but in which case the triggered ability fails and fizzles) is incorrect behavior [not sure].
2017-08-11 08:17:31 +00:00
Agetian
110a078074 - Cleaned up Mairsil a bit more. 2017-08-10 17:15:22 +00:00
Agetian
85b222d9e2 - Cosmetic reorder of target types in Mairsil. 2017-08-10 17:14:47 +00:00
Agetian
c974d4f30a - [C17] Added Mairsil, the Pretender. 2017-08-10 17:10:40 +00:00
Agetian
75916a21a6 - Removed an unused reference. 2017-08-10 16:24:03 +00:00
Agetian
8e2fc40105 - Updated Mathas, Fiend Seeker. 2017-08-10 16:23:01 +00:00
Agetian
f81bd857ed - Updated the text for Kindred Summons. 2017-08-10 16:04:19 +00:00
Agetian
e7ac05db75 - [C17] added 12 cards coded by Marek. 2017-08-10 16:03:20 +00:00
Agetian
eb3f526a96 - Fixed ChangeTextEffect with specific new text. 2017-08-10 15:47:04 +00:00
Agetian
371b0bdce1 - Committing card script corrections by Marek. 2017-08-10 13:48:11 +00:00
Maxmtg
06e70e7476 clean up some unused imports, add final modifier to parameter used in a closure 2017-08-10 13:05:41 +00:00
Agetian
a748fe6610 - Fixed Arahbo, Roar of the World. 2017-08-10 09:35:08 +00:00
Agetian
f43dd4d3dc - Some fixes for puzzle PC_072115 [part 3]. 2017-08-10 05:44:11 +00:00
Agetian
9c01b18922 - Some fixes for puzzle PC_072115 [part 2]. 2017-08-10 05:43:30 +00:00
Agetian
932fd032e8 - Some fixes for puzzle PC_072115. 2017-08-10 05:43:02 +00:00
Hanmac
c392deaf38 GameAction: checkStaticAbilities do CDA first 2017-08-10 04:59:07 +00:00
Hanmac
c575c1bea3 game state: counters are enum map, using hash map might break something 2017-08-10 04:34:22 +00:00
Agetian
38ec5efa32 - Puzzle PC_072115: marked Rhox Maulers as Renowned. 2017-08-10 04:12:31 +00:00
Agetian
70764e73c3 - Added three puzzles coded by Xitax. 2017-08-10 04:07:47 +00:00
Agetian
03fd7fba79 - [C17] added Fractured Identity. 2017-08-09 13:17:38 +00:00
Agetian
33d56a287b - Do not show an empty "Remembered:" tag on cards on which ClearRemembered was run. 2017-08-09 13:16:41 +00:00
Agetian
c3523f93aa - Reverted an occasional commit. 2017-08-09 12:52:01 +00:00
Agetian
26f08c7a30 - [C17] added 7 cards coded by Marek. 2017-08-09 12:51:21 +00:00
Agetian
a3266cda45 - Committing card script corrections from Marek. 2017-08-09 12:49:59 +00:00
Agetian
80052fabe8 - Minor formatting fix. 2017-08-09 10:20:43 +00:00
Agetian
3819a845f0 - Improved token pictures for tokens generated by Fabricate. 2017-08-09 10:12:34 +00:00
Agetian
d66c0f2d61 - Fixed AF DestroyAll destroying cards that can't be destroyed in case those cards were granted indestructibility by another card that is destroyed first (e.g. Crested Sunmare + horse tokens and an attempted removal via a Wrath of God type effect). 2017-08-09 09:55:25 +00:00
Agetian
0bf9da71ab - Fixed Traverse the Outlands casting cost. 2017-08-08 14:59:16 +00:00
Agetian
a07ff51c68 - [C17] Added 9 cards implemented by Marek (all tested ingame). 2017-08-08 12:23:56 +00:00
Agetian
48faabee49 - Committing card script corrections by Marek. 2017-08-08 11:38:27 +00:00
Agetian
d2f9eeab12 - Rudimentary prediction for Insult // Injury double damage effect for the AI (currently done in a way similar to how several other effects are predicted, which is (a) suboptimal - needs to be figured out from the replacement effect itself; (b) needs to be moved out from the Card and Player classes into the AI class, probably ComputerUtilCombat). Feel free to improve. 2017-08-08 10:00:04 +00:00
Agetian
c8ba4f0379 - Added AI logic parameter to Duskmantle, House of Shadow 2017-08-08 09:29:56 +00:00
Agetian
fba0fe1866 - Preparing Forge for Android publish 1.6.1.004 [incremental/fixes]. 2017-08-08 05:16:16 +00:00
Agetian
9685b92bc9 - Fixed a NPE in QuestDuelReader. 2017-08-08 04:46:48 +00:00
Agetian
3ae9779eec - Removed a debug print line. 2017-08-08 04:45:12 +00:00
Agetian
ebbdc46305 - Fix imports. 2017-08-08 04:44:37 +00:00
Agetian
4fb1c866da - AI: Avoid infinitely activating AF Untap on another permanent that will then be used to untap the first one (e.g. 2x Kiora's Follower) 2017-08-08 04:44:05 +00:00
Agetian
431d5ef8a8 - Added support for Renowned and Monstrous X properties to GameState. 2017-08-08 04:12:49 +00:00
Agetian
174d1f7838 - Puzzle mode improvements: no triggers will now run when the game state is set up; triggers will run in combat if attackers are declared. 2017-08-08 04:07:18 +00:00
Hanmac
b57ecea305 kefnet should cleanup choosen card 2017-08-07 16:28:23 +00:00
Agetian
64f39dcb79 - Fixed Glarecaster and Mirrorwood Treefolk. 2017-08-07 08:01:14 +00:00
Agetian
86149145f6 - Extended support for single target precast spells in Puzzle Mode. 2017-08-07 07:42:33 +00:00
Agetian
f3c41e9a66 - Use the ";" delimiter for precast spell declarations. 2017-08-07 05:00:42 +00:00
Agetian
2a6723f8db - Fixed an occasional change. 2017-08-07 04:50:12 +00:00
Agetian
2bb8d8b52a - Some puzzle mode improvements. Added support for precasting simple (untargeted) spells at the beginning of the game via AIPrecast/HumanPrecast parameters. Added support for Flipped and Meld card states. 2017-08-07 04:49:24 +00:00
Agetian
ac86cf19a0 - Added puzzles PC_080415 and PC_081115 coded by Xitax. 2017-08-07 03:59:08 +00:00
Agetian
ae29fef6d4 - Solution attempt #2 for the delayed trigger activator bug: store the original delayed trigger activator and restore it before running the trigger if a stored value was found, in case it was previously overwritten by the AI routines (fixes e.g. Rainbow Vale). 2017-08-07 03:43:38 +00:00
Agetian
54486cb5be - Reverting 34942 for now, causes weird, hard to track side effects. Better solution needed. 2017-08-06 18:17:56 +00:00
Agetian
22e41df8ea - Fixed a NPE in HostedMatch. 2017-08-06 18:03:45 +00:00
Agetian
d6dfc2ffb6 - Attempting to fix a long-standing bug with the delayed triggers getting the wrong activator set at their resolution time (the AI was aggressively overwriting the activator via its SpellAbility simulation routines).
- Ensured that all callers of getOriginalAndAltCostAbilities both call setActivatingPlayer and then reset it to its original value if there was one after the simulation completes. Thus, an aggressive setActivatingPlayer inside getOriginalAndAltCostAbilities should not be necessary.
2017-08-06 11:52:03 +00:00
Agetian
cb9d0ce3ed - Rebalanced Vanellope von Schweetz 1 quest opponent.
- Added Ice King 1 quest opponent by tojammot.
2017-08-06 11:43:11 +00:00
Agetian
cef5117e29 - Fixed Guan Yu, Sainted Warrior [fixing an occasionally missed | ]. 2017-08-06 07:43:56 +00:00
Agetian
466af94499 - Fixed Guan Yu, Sainted Warrior. 2017-08-06 07:43:17 +00:00
Agetian
ab9dea6930 - Fixed Challenge achievements being awarded in Puzzle Mode. 2017-08-05 11:50:54 +00:00
Agetian
cc2c585a55 - Added missing break statement. 2017-08-05 10:18:51 +00:00
Agetian
f9b1a59368 - Minor goal specification according to description. 2017-08-05 10:17:04 +00:00
Agetian
23cbe917c2 - Improvements and fixes for the new puzzle goal. 2017-08-05 10:09:04 +00:00
Hanmac
971f9694da ForgeConstants: add missing Constants 2017-08-05 10:01:05 +00:00
Agetian
32f057fa26 - Added support for "Destroy Permanents" goal in puzzle mode.
- Added puzzle PC_042815 implemented by Xitax.
2017-08-05 09:58:27 +00:00
Agetian
8c7edaaca4 - Added support for "Destroy Permanents"/"Kill Creatures" goal in puzzle mode.
- Added puzzle PC_042815 implemented by Xitax.
2017-08-05 09:54:27 +00:00
Hanmac
205323200a PlaneswalkerArchivements and AltWinArchivements are not make in res/lists 2017-08-05 09:45:33 +00:00
Agetian
9b880eb669 - Fixed AI for Jace, Telepath Unbound -3 ability. 2017-08-05 03:53:16 +00:00
austinio7116
3ce53cedc8 Something funny happened when I committed this file - so trying again 2017-08-04 19:15:23 +00:00
austinio7116
37e44968dc Update to card-based random deck data to include post HOU Pro Tour decks 2017-08-04 19:13:28 +00:00
Agetian
4fb58bc005 - All Hallow's Eve trigger should act as an intervening if clause. 2017-08-04 17:29:51 +00:00
Agetian
b24832dd84 - All Hallow's Eve must return the creatures to the battlefield once the last counter is removed (without necessarily waiting till the next upkeep). 2017-08-04 17:20:41 +00:00
Agetian
57aa32f0c1 - Added Bow to My Command.
- Currently probably one of the most complicated scripts, utilizing a couple hacks to simulate the "tap creatures with power 8 or greater" triggered ability. Most likely needs, at least, an UnlessCost update to support the tapXType cost with total creature power, and also potentially another update when/if shared turns and shared decisions are implemented.
2017-08-04 16:15:28 +00:00
Agetian
60c20cc628 - Added Choose Your Demise. 2017-08-04 15:57:05 +00:00
Agetian
eaae999878 - Fixed Jace, Architect of Thought -2 ability not allowing to order the cards going to the bottom of the library. 2017-08-04 15:54:31 +00:00
Agetian
f0af71d8c0 - Removed an experimental test line.
- Fixed Hazduhr the Abbot out of sight trigger definition.
2017-08-04 10:39:05 +00:00
Agetian
b65fed7a0d - Minor fix in description generation. 2017-08-04 10:23:45 +00:00
Agetian
364205cd9b - Added Hazduhr the Abbot. 2017-08-04 10:22:41 +00:00
Agetian
081ea769a1 - Removed an unused variable. 2017-08-04 08:46:48 +00:00
Agetian
7fef69635f - Chain of Acid doesn't need a special AITgts specification. 2017-08-04 08:46:01 +00:00
Agetian
e3c3315873 - Improved the AI logic for Chain of Acid. 2017-08-04 08:43:47 +00:00
Agetian
bb0218d3c6 - Fixed the AI cheat-activating AF PutCounter with a sac cost without sacrificing anything (e.g. Extruder). 2017-08-04 07:27:41 +00:00
Agetian
4e3e721c5a - Fixed Imaginary Threats. 2017-08-04 04:27:10 +00:00
Agetian
79321a0ffb - AlwaysPlayAi: implement an overriding confirmAction (set to return always true since AlwaysPlay is meant to do exactly that). 2017-08-03 19:52:48 +00:00
Agetian
230644141a - Removed a debug print line. 2017-08-03 19:40:29 +00:00
Agetian
480fa113b7 - Account for "schemes can't be set in motion this turn" when trying to set the scheme in motion again. 2017-08-03 19:39:44 +00:00
Agetian
9016d937a3 - Added My Laughter Echoes.
- Some improvements to AF SetInMotion which may be necessary to support Bow To My Command (seems almost scriptable, but the tap unless cost won't work).
2017-08-03 19:38:40 +00:00
Agetian
a361576f8a - Added A Reckoning Approaches. 2017-08-03 17:44:48 +00:00
Agetian
bee695afd4 - Removed unnecessary cleanup from My Forces Are Innumerable. 2017-08-03 16:36:41 +00:00
Agetian
f3fcdf808f - Added My Forces Are Innumerable (may need extension if E01 shared turns and shared opponent decisions are ever implemented in Forge). 2017-08-03 16:36:05 +00:00
Agetian
8e469493ac - Added Liege of the Hollows. 2017-08-03 16:03:38 +00:00
Agetian
872df9ee70 - Added Make Yourself Useful. 2017-08-03 15:05:34 +00:00
Agetian
69806f1260 - Added Errant Minion. 2017-08-03 14:35:50 +00:00
Agetian
0a24feab13 - Corrected Chain of Silence picture SVar. 2017-08-03 14:30:21 +00:00
Agetian
e40766583e - Added Every Dream a Nightmare. 2017-08-03 14:29:54 +00:00
Agetian
fd3fb5041b - Added Chain of Silence. 2017-08-03 13:56:30 +00:00
Agetian
8ffcaf328e - Added Delight in the Hunt. 2017-08-03 13:38:59 +00:00
Agetian
5381e54469 - Corrected Chain of Acid oracle text and picture SVar. 2017-08-03 13:32:45 +00:00
Agetian
f571685648 - Chain of Smog should be optional.
- Added Chain of Acid.
2017-08-03 13:29:14 +00:00
Agetian
701d363e3c - FIXME: correct the host card of a RepeatSubAbility since sometimes it's set incorrectly after an interaction, such as after a copy effect has been applied to a SA with Repeat/RepeatEach (e.g. the original Clone Legion not resolving correctly after its copy from Swarm Intelligence resolves). Couldn't figure out why this issue is happening, please assist if possible in determining the source of the problem and fixing it such that this workaround hack is unnecessary. 2017-08-03 13:00:06 +00:00
Agetian
afa697031c - A more comprehensive set of origin zones for Worms of the Earth replacement effect (probably superfluous, but accounts for potentially possible weird interactions). 2017-08-03 12:55:39 +00:00
Agetian
7239c98a4c - Worms of the Earth ruling: if a permanent spell tries to ETB (from stack) as a land, it goes into its owner's graveyard instead. 2017-08-03 12:54:09 +00:00
Agetian
48c0297fe7 - Fixed Abandoned Sarcophagus activating from zones other than the battlefield. 2017-08-02 19:14:41 +00:00
Agetian
a02be14f1a - Preparing Forge for Android publish 1.6.1.003 [incremental/fixes]. 2017-08-02 07:35:57 +00:00
Agetian
a0b8c758b4 - Fixed a crash in mobile Forge when trying to display the deck conformance error message outside of the Edt thread. 2017-08-02 07:31:52 +00:00
Agetian
97a8029f74 - Formatting fix. 2017-08-02 04:33:49 +00:00
Agetian
4d6781b26b - Added two new puzzles by Xitax and updated/fixed another one. 2017-08-02 04:33:23 +00:00
Agetian
b2d24725de - Simplify Worms of the Earth code. 2017-08-02 04:20:24 +00:00
Krazy
b04ac67860 Make booster quest pool generation respect starting pool set selections 2017-08-02 01:42:01 +00:00
Agetian
61784fd48d - Documenting changes in CHANGES.txt. 2017-08-01 18:22:59 +00:00
Agetian
f29738a96b - Added For Each of You, a Gift. 2017-08-01 17:36:15 +00:00
Agetian
cb313ed513 - Some text update in Worms of the Earth. 2017-08-01 17:22:25 +00:00
Agetian
e23656a2cd - Added Worms of the Earth. 2017-08-01 17:20:50 +00:00
Agetian
8a39ff469f - Added Power Leak (for now, needs a special exclusion in the AI code to properly determine who the paying player is, otherwise the AI believes that it's the opponent who owns Power Leak in case it's cast by the opponent). 2017-08-01 16:52:26 +00:00
Agetian
89c369189a - Added two puzzles by Rooger that are now scriptable. 2017-08-01 10:29:37 +00:00
Agetian
1108c92beb - Fixed the timing of the puzzle description popup in desktop Forge. 2017-08-01 10:25:09 +00:00
Agetian
e43cfce016 - Improved and fixed the handling of game states inside combat phases (DA, DB), added a way to mark cards that are attacking in saved game states. 2017-08-01 10:10:15 +00:00
Agetian
5931b53896 - Removed an unused variable. 2017-08-01 08:26:35 +00:00
Agetian
020b31cb6f - Added goal type "Survive" to puzzle mode.
- Added two new puzzles from Rooger that are now scriptable.
2017-08-01 06:37:05 +00:00
Agetian
8fc41c4c45 - Added several puzzles by Rooger that are now scriptable. 2017-08-01 06:21:44 +00:00
Agetian
d9b3c2c15c - Added a comment. 2017-08-01 06:17:44 +00:00
Agetian
10a9825f82 - Fixed a bug that caused multiple attachments on the same permanent not to work in game states.
- Improved the game state support to handle remembered cards and ExiledWith.
2017-08-01 06:16:46 +00:00
Agetian
2bbe168200 - Script execution in GameState: look for other scripts to execute if one of the SVars was not found. 2017-07-31 12:01:53 +00:00
Agetian
d6e8a96b19 - Script execution in GameState: do not iterate over all SVars, just grab the necessary SVar directly. 2017-07-31 12:00:02 +00:00
Agetian
c34fae302e - Added a way to force execution of a portion of card script when setting up game state (needed for some puzzles that utilize cards that dynamically set up effects when they ETB, e.g. Suspension Field).
- Added PC_041415 puzzle by Xitax.
2017-07-31 05:27:15 +00:00
Agetian
7957135979 - Some description fixes in puzzles. 2017-07-31 04:57:14 +00:00
Agetian
c9330d0b7f - Added Portal Second Age quest world by Xyx. 2017-07-31 04:01:59 +00:00
Agetian
9df5fc12cd Added PC_063015 puzzle by Xitax. 2017-07-31 03:43:37 +00:00
Sol
dd64685049 Fix typo in Oasis Ritualist 2017-07-30 23:58:06 +00:00
Hanmac
eb54b90001 blade of the bloodchief: add missing References 2017-07-30 20:04:04 +00:00
Agetian
b1ba5790cc - Added some puzzles implemented by Xitax. Updated the naming scheme for puzzles from GatheringMagic.com. 2017-07-30 15:02:49 +00:00
Agetian
56d79f36dc - Fixed a crash when loading a game state with a misspelled card name (will now report the card name in the console instead of hard-crashing). 2017-07-30 11:14:32 +00:00
Agetian
bbcf7b638f - Added a TODO entry to GameState. 2017-07-30 10:47:27 +00:00
Agetian
94cc314738 - Preserve ChosenColors and ChosenType in game states. 2017-07-30 10:40:32 +00:00
Agetian
39eb6482e3 - Handle marked damage before the triggers are unsuppressed when applying game states. 2017-07-30 10:19:46 +00:00
Agetian
85e66ebd8a - Fixed marking damage in puzzle mode and game states.
- Fixed card attachment in puzzle mode not working correctly when attaching a card to a card belonging to a different player.
2017-07-30 08:27:16 +00:00
Agetian
322b7084ea - Use calculateAmount in AttachAi instead of a custom method. 2017-07-30 06:53:52 +00:00
Agetian
8ac2c0462a - Fixed a NPE in AttachAi related to processing negative count for X (e.g. -X in Quag Sickness). 2017-07-30 06:40:17 +00:00
Hanmac
24e2e29894 CardFactoryUtil: fixed Eternalize and nonManaCost 2017-07-30 06:07:43 +00:00
Sol
e00aeb39e5 Fix Manalith rarity 2017-07-29 23:41:10 +00:00
Hanmac
6ec2849bd5 fixed god pharaoh's gift, the haste is only until end of turm 2017-07-29 19:56:18 +00:00
Hanmac
698c9d2923 PlayAi: fixed non-final error 2017-07-29 19:53:28 +00:00
Hanmac
82f379ecbe fixed gate to the afterlife 2017-07-29 17:23:14 +00:00
Agetian
a502ceea53 - Added a way to preserve marked damage in game states and puzzles. 2017-07-29 17:04:26 +00:00
Agetian
53e4b39066 - Prevent a NPE in booster foil generation when the template does not contain any slots. 2017-07-29 13:47:50 +00:00
Agetian
fceea79323 - Fixed interaction between put/remove counter as a part of cost payment and cards that trigger on things dying from counters (e.g. Necroskitter + a creature dying to a -1/-1 counter placed as a part of cost payment). 2017-07-29 13:36:09 +00:00
Agetian
6adf49de45 Fixed AILogic$ Evasion for EffectAi causing the AI to play the relevant cards out of context (e.g. Gruul Charm with the "can't block" mode on an empty battlefield). 2017-07-29 11:42:46 +00:00
Hanmac
0b85346ac4 TriggerHandler: try to fix Splendid Reclamation and Valakut 2017-07-29 05:11:22 +00:00
Agetian
53f0544da8 - Alternative Cost for spells should be added to all spells on the card, not only the first spell ability (fixes interaction with split cards, among possibly other things). 2017-07-28 13:45:14 +00:00
Agetian
839ced1b32 - Use a separate AI logic for "Detain target nonland permanent". 2017-07-28 07:44:02 +00:00
Agetian
b6fcbba57d - Preparing Forge for Android publish 1.6.1.002 [incremental/bug fixes]. 2017-07-28 04:24:43 +00:00
Agetian
b7f220d02b - Formatting fix. 2017-07-28 04:20:19 +00:00
Agetian
398ae8946c - Fixed the AI targeting nonland permanents with no activated abilities with effects like Detain. 2017-07-28 04:19:06 +00:00
Agetian
65357e1441 - Added PC_07 puzzle by Xitax. Differentiated between early and late Perplexing Chimera puzzles in their naming scheme. 2017-07-28 03:26:03 +00:00
Agetian
0a7f579bd8 - Fixed split cards having transform arrows in deck editor. 2017-07-27 19:23:50 +00:00
Agetian
2e0d2bb5e5 - Minor formatting fix. 2017-07-27 18:31:51 +00:00
Agetian
0612d447dc - Fixed generation of Wastes in OGW fat packs and an associated crash when trying to foil a non-existent Wastes in such a fat pack. 2017-07-27 18:28:37 +00:00
Agetian
fd7e19d339 - Fixed the unfoiling of cards displayed in booster boxes, fat packs, etc. (take two) 2017-07-27 17:54:59 +00:00
Agetian
7f8dec161d - Minor clarification for Puzzle Mode on mobile. 2017-07-27 05:55:38 +00:00
Agetian
26f50d109a - Show the puzzle description in a pop-up dialog window when the puzzle starts. 2017-07-27 04:29:28 +00:00
Agetian
acfdf23c22 - Fixed the player's max hand size in Puzzle Mode being equal to 0 by default (now set to 7 per the default MTG rules).
- Fixed the "Turns:X" parameter not working correctly in Puzzle Mode.
2017-07-26 12:58:04 +00:00
Agetian
a15588120b - Attempting to fix IndexOutOfBounds exception in GauntletWinLose 2017-07-26 04:18:13 +00:00
Agetian
da28816967 - Updating cards with AddPower/AddToughness$ -X 2017-07-25 19:17:44 +00:00
Agetian
53bf9922ca - Added several new puzzles by Xitax. 2017-07-25 16:06:01 +00:00
Agetian
363ff9610d - Some comment update. 2017-07-25 16:02:12 +00:00
Agetian
772c9bc77d - Updated the test case for Death's Shadow. 2017-07-25 15:51:47 +00:00
Agetian
c1f10c32c0 - Optimized SVars in Death's Shadow. 2017-07-25 15:49:54 +00:00
Agetian
0a8c36e086 - Fixed Death's Shadow implementation to work correctly with the new StaticAbilityContinuous modification. 2017-07-25 15:48:54 +00:00
Agetian
ae35e4b589 - Added a test case for Death's Shadow on negative life under the new rules.
- Some clarifications in recent tests.
2017-07-25 15:32:10 +00:00
Agetian
92a760541b - Fixed StaticAbilityContinuous applying negative P/T bonuses for cards like Death's Shadow when player's life was negative (incorrect under the new rules). 2017-07-25 15:13:14 +00:00
Agetian
3c67546f4a - Fixed Desert's Hold AI prioritizing wrong targets. 2017-07-25 14:57:55 +00:00
Agetian
695670cc96 - Fixed Wall of Forgotten Pharaohs generated description. 2017-07-25 14:43:08 +00:00
Agetian
0f42aa4ec7 - Fixed Mechanized Production AI targeting legendary artifacts to no value. 2017-07-25 11:54:01 +00:00
Agetian
8aca69f845 - Fixed the foil effect in boosters not "unfoiling" itself for multiple cards with the same name in the same card set (e.g. in a booster box). 2017-07-25 03:28:51 +00:00
Agetian
63be38a3c3 - Added an ability to show puzzle descriptions on the puzzle goal card. 2017-07-25 03:14:40 +00:00
Agetian
e7f6e5c740 - Fixed Hour of Devastation number of booster pictures. 2017-07-25 03:13:45 +00:00
Agetian
2ae8d49ec8 - Fixed "Auto Yield: Always No" in mobile Forge. 2017-07-25 03:13:11 +00:00
Agetian
f9e987e933 - Reverted several Java 8 functions to their Java 7 counterparts for Android compatibility. 2017-07-25 03:12:04 +00:00
Agetian
8e9c76a9e8 - Fixed Mummy Paramount (non-optional). 2017-07-25 03:10:17 +00:00
Agetian
ad52b60798 - Preparing Forge for Android publish 1.6.1.001 [incremental]. 2017-07-22 03:44:09 +00:00
Blacksmith
012cc28f8a Clear out release files in preparation for next release 2017-07-22 00:51:42 +00:00
Blacksmith
1668717cf8 [maven-release-plugin] prepare for next development iteration 2017-07-22 00:46:25 +00:00
2016 changed files with 29916 additions and 9837 deletions

656
.gitattributes vendored

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>1.6.1</version> <version>1.6.4</version>
</parent> </parent>
<artifactId>forge-ai</artifactId> <artifactId>forge-ai</artifactId>

View File

@@ -1,4 +1,4 @@
package forge; package forge.ai;
public enum AIOption { public enum AIOption {
USE_SIMULATION; USE_SIMULATION;

View File

@@ -17,14 +17,9 @@
*/ */
package forge.ai; package forge.ai;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.ability.AnimateAi; import forge.ai.ability.AnimateAi;
import forge.card.CardTypeView; import forge.card.CardTypeView;
import forge.game.GameEntity; import forge.game.GameEntity;
@@ -40,9 +35,14 @@ import forge.game.spellability.SpellAbility;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Expressions;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
//doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer() //doesHumanAttackAndWin() uses the global variable AllZone.getComputerPlayer()
/** /**
@@ -243,6 +243,19 @@ public class AiAttackController {
return false; return false;
} }
public final static Card getCardCanBlockAnAttacker(final Card c, final List<Card> attackers, final boolean nextTurn) {
final List<Card> attackerList = new ArrayList<Card>(attackers);
if (!c.isCreature()) {
return null;
}
for (final Card attacker : attackerList) {
if (CombatUtil.canBlock(attacker, c, nextTurn)) {
return attacker;
}
}
return null;
}
// this checks to make sure that the computer player doesn't lose when the human player attacks // this checks to make sure that the computer player doesn't lose when the human player attacks
// this method is used by getAttackers() // this method is used by getAttackers()
public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) { public final List<Card> notNeededAsBlockers(final Player ai, final List<Card> attackers) {
@@ -362,8 +375,12 @@ public class AiAttackController {
blockersLeft--; blockersLeft--;
continue; continue;
} }
totalAttack += ComputerUtilCombat.damageIfUnblocked(attacker, ai, null, false);
totalPoison += ComputerUtilCombat.poisonIfUnblocked(attacker, ai); // Test for some special triggers that can change the creature in combat
Card effectiveAttacker = ComputerUtilCombat.applyPotentialAttackCloneTriggers(attacker);
totalAttack += ComputerUtilCombat.damageIfUnblocked(effectiveAttacker, ai, null, false);
totalPoison += ComputerUtilCombat.poisonIfUnblocked(effectiveAttacker, ai);
} }
if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) { if (totalAttack > 0 && ai.getLife() <= totalAttack && !ai.cantLoseForZeroOrLessLife()) {
@@ -424,16 +441,45 @@ public class AiAttackController {
final Player opp = this.defendingOpponent; final Player opp = this.defendingOpponent;
for (Card attacker : attackers) { // if true, the AI will attempt to identify which blockers will already be taken,
if (!CombatUtil.canBeBlocked(attacker, this.blockers, null) // thus attempting to predict how many creatures with evasion can actively block
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) { boolean predictEvasion = false;
unblockedAttackers.add(attacker); if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
if (aic.getBooleanProperty(AiProps.COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION)) {
predictEvasion = true;
} }
} }
CardCollection accountedBlockers = new CardCollection(this.blockers);
CardCollection categorizedAttackers = new CardCollection();
if (predictEvasion) {
// split categorizedAttackers such that the ones with evasion come first and
// can be properly accounted for. Note that at this point the attackers need
// to be sorted by power already (see the Collections.sort call above).
categorizedAttackers.addAll(ComputerUtilCombat.categorizeAttackersByEvasion(this.attackers));
} else {
categorizedAttackers.addAll(this.attackers);
}
for (Card attacker : categorizedAttackers) {
if (!CombatUtil.canBeBlocked(attacker, accountedBlockers, null)
|| attacker.hasKeyword("You may have CARDNAME assign its combat damage as though it weren't blocked.")) {
unblockedAttackers.add(attacker);
} else {
if (predictEvasion) {
List<Card> potentialBestBlockers = CombatUtil.getPotentialBestBlockers(attacker, accountedBlockers, null);
accountedBlockers.removeAll(potentialBestBlockers);
}
}
}
remainingAttackers.removeAll(unblockedAttackers);
for (Card blocker : this.blockers) { for (Card blocker : this.blockers) {
if (blocker.hasKeyword("CARDNAME can block any number of creatures.") if (blocker.hasKeyword("CARDNAME can block any number of creatures.")
|| blocker.hasKeyword("CARDNAME can block an additional ninety-nine creatures.")) { || blocker.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
for (Card attacker : this.attackers) { for (Card attacker : this.attackers) {
if (CombatUtil.canBlock(attacker, blocker)) { if (CombatUtil.canBlock(attacker, blocker)) {
remainingAttackers.remove(attacker); remainingAttackers.remove(attacker);
@@ -449,7 +495,7 @@ public class AiAttackController {
if (remainingAttackers.isEmpty() || maxBlockersAfterCrew == 0) { if (remainingAttackers.isEmpty() || maxBlockersAfterCrew == 0) {
break; break;
} }
if (blocker.hasKeyword("CARDNAME can block an additional creature.")) { if (blocker.hasKeyword("CARDNAME can block an additional creature each combat.")) {
blockedAttackers.add(remainingAttackers.get(0)); blockedAttackers.add(remainingAttackers.get(0));
remainingAttackers.remove(0); remainingAttackers.remove(0);
maxBlockersAfterCrew--; maxBlockersAfterCrew--;
@@ -498,11 +544,16 @@ public class AiAttackController {
} }
Player prefDefender = (Player) (defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0)); Player prefDefender = (Player) (defs.contains(this.defendingOpponent) ? this.defendingOpponent : defs.get(0));
final GameEntity entity = ai.getMustAttackEntity(); // Attempt to see if there's a defined entity that must be attacked strictly this turn...
GameEntity entity = ai.getMustAttackEntityThisTurn();
if (entity == null) {
// ...or during the attacking creature controller's turn
entity = ai.getMustAttackEntity();
}
if (null != entity) { if (null != entity) {
int n = defs.indexOf(entity); int n = defs.indexOf(entity);
if (-1 == n) { if (-1 == n) {
System.out.println("getMustAttackEntity() returned something not in defenders."); System.out.println("getMustAttackEntity() or getMustAttackEntityThisTurn() returned something not in defenders.");
return prefDefender; return prefDefender;
} else { } else {
return entity; return entity;
@@ -544,10 +595,21 @@ public class AiAttackController {
return; return;
} }
// Aggro options
boolean playAggro = false; boolean playAggro = false;
int chanceToAttackToTrade = 0;
boolean tradeIfTappedOut = false;
int extraChanceIfOppHasMana = 0;
boolean tradeIfLowerLifePressure = false;
if (ai.getController().isAI()) { if (ai.getController().isAI()) {
playAggro = ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true"); AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
playAggro = aic.getBooleanProperty(AiProps.PLAY_AGGRO);
chanceToAttackToTrade = aic.getIntProperty(AiProps.CHANCE_TO_ATTACK_INTO_TRADE);
tradeIfTappedOut = aic.getBooleanProperty(AiProps.ATTACK_INTO_TRADE_WHEN_TAPPED_OUT);
extraChanceIfOppHasMana = aic.getIntProperty(AiProps.CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA);
tradeIfLowerLifePressure = aic.getBooleanProperty(AiProps.RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE);
} }
final boolean bAssault = this.doAssault(ai); final boolean bAssault = this.doAssault(ai);
// TODO: detect Lightmine Field by presence of a card with a specific trigger // TODO: detect Lightmine Field by presence of a card with a specific trigger
final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field"); final boolean lightmineField = ComputerUtilCard.isPresentOnBattlefield(ai.getGame(), "Lightmine Field");
@@ -558,7 +620,11 @@ public class AiAttackController {
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions? // TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders()); GlobalAttackRestrictions restrict = GlobalAttackRestrictions.getGlobalRestrictions(ai, combat.getDefenders());
final int attackMax = restrict.getMax(); int attackMax = restrict.getMax();
if (attackMax == -1) {
// check with the local limitations vs. the chosen defender
attackMax = ComputerUtilCombat.getMaxAttackersFor(defender);
}
if (attackMax == 0) { if (attackMax == 0) {
// can't attack anymore // can't attack anymore
@@ -590,7 +656,7 @@ public class AiAttackController {
} }
} }
} }
if (mustAttack || attacker.getController().getMustAttackEntity() != null) { if (mustAttack || attacker.getController().getMustAttackEntity() != null || attacker.getController().getMustAttackEntityThisTurn() != null) {
combat.addAttacker(attacker, defender); combat.addAttacker(attacker, defender);
attackersLeft.remove(attacker); attackersLeft.remove(attacker);
numForcedAttackers++; numForcedAttackers++;
@@ -711,7 +777,20 @@ public class AiAttackController {
} }
} }
for (final Card pCard : this.oppList) { boolean predictEvasion = (ai.getController().isAI()
&& ((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION));
CardCollection categorizedOppList = new CardCollection();
if (predictEvasion) {
// If predicting evasion, make sure that attackers with evasion are considered first
// (to avoid situations where the AI would predict his non-flyers to be blocked with
// flying creatures and then believe that flyers will necessarily be left unblocked)
categorizedOppList.addAll(ComputerUtilCombat.categorizeAttackersByEvasion(this.oppList));
} else {
categorizedOppList.addAll(this.oppList);
}
for (final Card pCard : categorizedOppList) {
// if the creature can attack next turn add it to counter attackers list // if the creature can attack next turn add it to counter attackers list
if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) { if (ComputerUtilCombat.canAttackNextTurn(pCard) && pCard.getNetCombatDamage() > 0) {
nextTurnAttackers.add(pCard); nextTurnAttackers.add(pCard);
@@ -719,8 +798,13 @@ public class AiAttackController {
humanForces += 1; // player forces they might use to attack humanForces += 1; // player forces they might use to attack
} }
// increment player forces that are relevant to an attritional attack - includes walls // increment player forces that are relevant to an attritional attack - includes walls
if (canBlockAnAttacker(pCard, candidateAttackers, true)) {
Card potentialOppBlocker = getCardCanBlockAnAttacker(pCard, candidateAttackers, true);
if (potentialOppBlocker != null) {
humanForcesForAttritionalAttack += 1; humanForcesForAttritionalAttack += 1;
if (predictEvasion) {
candidateAttackers.remove(potentialOppBlocker);
}
} }
} }
@@ -847,8 +931,18 @@ public class AiAttackController {
if (ratioDiff > 0 && doAttritionalAttack) { if (ratioDiff > 0 && doAttritionalAttack) {
this.aiAggression = 5; // attack at all costs this.aiAggression = 5; // attack at all costs
} else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0)) } else if ((ratioDiff >= 1 && this.attackers.size() > 1 && (humanLifeToDamageRatio < 2 || outNumber > 0))
|| (playAggro && humanLifeToDamageRatio > 1)) { || (playAggro && MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1)) {
this.aiAggression = 4; // attack expecting to trade or damage player. this.aiAggression = 4; // attack expecting to trade or damage player.
} else if (MyRandom.percentTrue(chanceToAttackToTrade) && humanLifeToDamageRatio > 1
&& defendingOpponent != null
&& ComputerUtil.countUsefulCreatures(ai) > ComputerUtil.countUsefulCreatures(defendingOpponent)
&& ai.getLife() > defendingOpponent.getLife()
&& !ComputerUtilCombat.lifeInDanger(ai, combat)
&& (ComputerUtilMana.getAvailableManaEstimate(ai) > 0) || tradeIfTappedOut
&& (ComputerUtilMana.getAvailableManaEstimate(defendingOpponent) == 0) || MyRandom.percentTrue(extraChanceIfOppHasMana)
&& (!tradeIfLowerLifePressure || (ai.getLifeLostLastTurn() + ai.getLifeLostThisTurn() <
defendingOpponent.getLifeLostThisTurn() + defendingOpponent.getLifeLostThisTurn()))) {
this.aiAggression = 4; // random (chance-based) attack expecting to trade or damage player.
} else if (ratioDiff >= 0 && this.attackers.size() > 1) { } else if (ratioDiff >= 0 && this.attackers.size() > 1) {
this.aiAggression = 3; // attack expecting to make good trades or damage player. this.aiAggression = 3; // attack expecting to make good trades or damage player.
} else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1 } else if (ratioDiff + outNumber >= -1 || aiLifeToPlayerDamageRatio > 1
@@ -996,7 +1090,8 @@ public class AiAttackController {
|| "Blocked".equals(attacker.getSVar("HasAttackEffect")); || "Blocked".equals(attacker.getSVar("HasAttackEffect"));
if (!hasCombatEffect) { if (!hasCombatEffect) {
for (String keyword : attacker.getKeywords()) { for (String keyword : attacker.getKeywords()) {
if (keyword.equals("Wither") || keyword.equals("Infect") || keyword.equals("Lifelink")) { if (keyword.equals("Wither") || keyword.equals("Infect")
|| keyword.equals("Lifelink") || keyword.startsWith("Afflict")) {
hasCombatEffect = true; hasCombatEffect = true;
break; break;
} }
@@ -1037,12 +1132,28 @@ public class AiAttackController {
// and combat will have negative effects // and combat will have negative effects
} }
} }
if (canKillAllDangerous
&& !hasAttackEffect && !hasCombatEffect
&& (this.attackers.size() <= defenders.size() || attacker.getNetPower() <= 0)) {
if (ai.getController().isAI()) {
if (((PlayerControllerAi)ai.getController()).getAi().getBooleanProperty(AiProps.TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK)) {
// We can't kill a blocker, there is no reason to attack unless we can cripple a
// blocker or gain life from attacking or we have some kind of another attack/combat effect,
// or if we can deal damage to the opponent via the sheer number of potential attackers
// (note that the AI will sometimes still miscount here, and thus attack into a block,
// because there is no way to check which attackers are actually guaranteed to attack at this point)
canKillAllDangerous = false;
}
}
}
} }
} }
} }
} }
if (!attacker.hasKeyword("vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) { if (!attacker.hasKeyword("Vigilance") && ComputerUtilCard.canBeKilledByRoyalAssassin(ai, attacker)) {
canKillAllDangerous = false; canKillAllDangerous = false;
canBeKilled = true; canBeKilled = true;
canBeKilledByOne = true; canBeKilledByOne = true;
@@ -1147,18 +1258,51 @@ public class AiAttackController {
} }
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.setActivatingPlayer(c.getController()); sa.setActivatingPlayer(c.getController());
if (CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa).isEmpty()) { List<Card> validTargets = CardUtil.getValidCardsToTarget(sa.getTargetRestrictions(), sa);
if (validTargets.isEmpty()) {
missTarget = true;
break;
} else if (sa.isCurse() && CardLists.filter(validTargets,
CardPredicates.isControlledByAnyOf(c.getController().getOpponents())).isEmpty()) {
// e.g. Ahn-Crop Crasher - the effect is only good when aimed at opponent's creatures
missTarget = true; missTarget = true;
break; break;
} }
} }
} }
if (missTarget) { if (missTarget) {
continue; continue;
} }
if (random.nextBoolean()) { // A specific AI condition for Exert: if specified on the card, the AI will always
// exert creatures that meet this condition
if (c.hasSVar("AIExertCondition")) {
if (!c.getSVar("AIExertCondition").isEmpty()) {
final String needsToExert = c.getSVar("AIExertCondition");
int x = 0;
int y = 0;
String sVar = needsToExert.split(" ")[0];
String comparator = needsToExert.split(" ")[1];
String compareTo = comparator.substring(2);
try {
x = Integer.parseInt(sVar);
} catch (final NumberFormatException e) {
x = CardFactoryUtil.xCount(c, c.getSVar(sVar));
}
try {
y = Integer.parseInt(compareTo);
} catch (final NumberFormatException e) {
y = CardFactoryUtil.xCount(c, c.getSVar(compareTo));
}
if (Expressions.compare(x, comparator, y)) {
shouldExert = true;
}
}
}
if (!shouldExert && random.nextBoolean()) {
// TODO Improve when the AI wants to use Exert powers // TODO Improve when the AI wants to use Exert powers
shouldExert = true; shouldExert = true;
} }

View File

@@ -19,26 +19,20 @@ package forge.ai;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import forge.card.CardStateName;
import forge.game.CardTraitBase; import forge.game.CardTraitBase;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import java.util.ArrayList; import java.util.*;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
/** /**
@@ -89,8 +83,10 @@ public class AiBlockController {
private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) { private List<Card> getSafeBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>(); final List<Card> blockers = new ArrayList<>();
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
for (final Card b : blockersLeft) { for (final Card b : blockersLeft) {
if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false)) { if (!ComputerUtilCombat.canDestroyBlocker(ai, b, attacker, combat, false, true)) {
blockers.add(b); blockers.add(b);
} }
} }
@@ -102,8 +98,10 @@ public class AiBlockController {
private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) { private List<Card> getKillingBlockers(final Combat combat, final Card attacker, final List<Card> blockersLeft) {
final List<Card> blockers = new ArrayList<>(); final List<Card> blockers = new ArrayList<>();
// We don't check attacker static abilities at this point since the attackers have already attacked and, thus,
// their P/T modifiers are active and are counted as a part of getNetPower/getNetToughness
for (final Card b : blockersLeft) { for (final Card b : blockersLeft) {
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false)) { if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, b, combat, false, true)) {
blockers.add(b); blockers.add(b);
} }
} }
@@ -151,7 +149,7 @@ public class AiBlockController {
for (final Card c : attackers) { for (final Card c : attackers) {
sortedAttackers.add(c); sortedAttackers.add(c);
} }
} else { } else if (defender instanceof Player && defender.equals(ai)){
firstAttacker = combat.getAttackersOf(defender); firstAttacker = combat.getAttackersOf(defender);
} }
} }
@@ -349,9 +347,9 @@ public class AiBlockController {
final List<Card> firstStrikeBlockers = new ArrayList<>(); final List<Card> firstStrikeBlockers = new ArrayList<>();
final List<Card> blockGang = new ArrayList<>(); final List<Card> blockGang = new ArrayList<>();
for (Card blocker : blockers) { for (Card blocker : blockers) {
if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) { if (ComputerUtilCombat.canDestroyBlockerBeforeFirstStrike(blocker, attacker, false)) {
continue; continue;
} }
if (blocker.hasFirstStrike() || blocker.hasDoubleStrike()) { if (blocker.hasFirstStrike() || blocker.hasDoubleStrike()) {
firstStrikeBlockers.add(blocker); firstStrikeBlockers.add(blocker);
} }
@@ -417,7 +415,8 @@ public class AiBlockController {
&& !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) { && !ComputerUtilCombat.dealsFirstStrikeDamage(c, false, combat)) {
return false; return false;
} }
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker); final boolean randomTrade = wouldLikeToRandomlyTrade(attacker, c, combat);
return lifeInDanger || ComputerUtilCard.evaluateCreature(c) + diff < ComputerUtilCard.evaluateCreature(attacker) || randomTrade;
} }
}); });
if (usableBlockers.size() < 2) { if (usableBlockers.size() < 2) {
@@ -445,9 +444,9 @@ public class AiBlockController {
// The attacker will be killed // The attacker will be killed
&& (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage() && (absorbedDamage2 + absorbedDamage > attacker.getNetCombatDamage()
// only one blocker can be killed // only one blocker can be killed
|| currentValue + addedValue - 50 <= evalAttackerValue || currentValue + addedValue - 50 <= evalAttackerValue
// or attacker is worth more // or attacker is worth more
|| (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat))) || (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)))
// or life is in danger // or life is in danger
&& CombatUtil.canBlock(attacker, blocker, combat)) { && CombatUtil.canBlock(attacker, blocker, combat)) {
// this is needed for attackers that can't be blocked by // this is needed for attackers that can't be blocked by
@@ -497,7 +496,7 @@ public class AiBlockController {
&& !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3) && !(damageNeeded > currentDamage + additionalDamage2 + additionalDamage3)
// The attacker will be killed // The attacker will be killed
&& ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage && ((absorbedDamage2 + absorbedDamage > netCombatDamage && absorbedDamage3 + absorbedDamage > netCombatDamage
&& absorbedDamage3 + absorbedDamage2 > netCombatDamage) && absorbedDamage3 + absorbedDamage2 > netCombatDamage)
// only one blocker can be killed // only one blocker can be killed
|| currentValue + addedValue2 + addedValue3 - 50 <= evalAttackerValue || currentValue + addedValue2 + addedValue3 - 50 <= evalAttackerValue
// or attacker is worth more // or attacker is worth more
@@ -526,7 +525,56 @@ public class AiBlockController {
attackersLeft = (new ArrayList<>(currentAttackers)); attackersLeft = (new ArrayList<>(currentAttackers));
} }
private void makeGangNonLethalBlocks(final Combat combat) {
List<Card> currentAttackers = new ArrayList<>(attackersLeft);
List<Card> blockers;
// Try to block a Menace attacker with two blockers, neither of which will die
for (final Card attacker : attackersLeft) {
if (!attacker.hasKeyword("Menace") && !attacker.hasStartOfKeyword("CantBeBlockedByAmount LT2")) {
continue;
}
blockers = getPossibleBlockers(combat, attacker, blockersLeft, false);
List<Card> usableBlockers;
final List<Card> blockGang = new ArrayList<>();
int absorbedDamage; // The amount of damage needed to kill the first blocker
usableBlockers = CardLists.filter(blockers, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return c.getNetToughness() > attacker.getNetCombatDamage();
}
});
if (usableBlockers.size() < 2) {
return;
}
final Card leader = ComputerUtilCard.getWorstCreatureAI(usableBlockers);
blockGang.add(leader);
usableBlockers.remove(leader);
absorbedDamage = ComputerUtilCombat.getEnoughDamageToKill(leader, attacker.getNetCombatDamage(), attacker, true);
// consider a double block
for (final Card blocker : usableBlockers) {
final int absorbedDamage2 = ComputerUtilCombat.getEnoughDamageToKill(blocker, attacker.getNetCombatDamage(), attacker, true);
// only do it if neither blocking creature will die
if (absorbedDamage > attacker.getNetCombatDamage() && absorbedDamage2 > attacker.getNetCombatDamage()) {
currentAttackers.remove(attacker);
combat.addBlocker(attacker, blocker);
if (CombatUtil.canBlock(attacker, leader, combat)) {
combat.addBlocker(attacker, leader);
}
break;
}
}
}
attackersLeft = (new ArrayList<>(currentAttackers));
}
// Bad Trade Blocks (should only be made if life is in danger) // Bad Trade Blocks (should only be made if life is in danger)
// Random Trade Blocks (performed randomly if enabled in profile and only when in favorable conditions)
/** /**
* <p> * <p>
* makeTradeBlocks. * makeTradeBlocks.
@@ -546,16 +594,29 @@ public class AiBlockController {
|| attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) { || attacker.hasKeyword("CARDNAME can't be blocked unless all creatures defending player controls block it.")) {
continue; continue;
} }
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) {
continue;
}
List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true); List<Card> possibleBlockers = getPossibleBlockers(combat, attacker, blockersLeft, true);
killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers); killingBlockers = getKillingBlockers(combat, attacker, possibleBlockers);
if (!killingBlockers.isEmpty() && ComputerUtilCombat.lifeInDanger(ai, combat)) {
if (ComputerUtilCombat.attackerHasThreateningAfflict(attacker, ai)) { if (!killingBlockers.isEmpty()) {
continue;
}
final Card blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers); final Card blocker = ComputerUtilCard.getWorstCreatureAI(killingBlockers);
combat.addBlocker(attacker, blocker); boolean doTrade = false;
currentAttackers.remove(attacker);
if (ComputerUtilCombat.lifeInDanger(ai, combat)) {
// Always trade when life in danger
doTrade = true;
} else {
// Randomly trade creatures with lower power and [hopefully] worse abilities, if enabled in profile
doTrade = wouldLikeToRandomlyTrade(attacker, blocker, combat);
}
if (doTrade) {
combat.addBlocker(attacker, blocker);
currentAttackers.remove(attacker);
}
} }
} }
attackersLeft = (new ArrayList<>(currentAttackers)); attackersLeft = (new ArrayList<>(currentAttackers));
@@ -760,6 +821,105 @@ public class AiBlockController {
} }
} }
private void makeChumpBlocksToSavePW(Combat combat) {
if (ComputerUtilCombat.lifeInDanger(ai, combat) || ai.getLife() <= ai.getStartingLife() / 5) {
// most likely not worth trying to protect planeswalkers when at threateningly low life or in
// dangerous combat which threatens lethal or severe damage to face
return;
}
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
final int evalThresholdToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final int evalThresholdNonToken = aic.getIntProperty(AiProps.THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER);
final boolean onlyIfLethal = aic.getBooleanProperty(AiProps.CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL);
if (evalThresholdToken > 0 || evalThresholdNonToken > 0) {
// detect how much damage is threatened to each of the planeswalkers, see which ones would be
// worth protecting according to the AI profile properties
CardCollection threatenedPWs = new CardCollection();
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card) {
if (!onlyIfLethal) {
threatenedPWs.add((Card) def);
} else {
int damageToPW = 0;
for (final Card pwatkr : combat.getAttackersOf(def)) {
if (!combat.isBlocked(pwatkr)) {
damageToPW += ComputerUtilCombat.predictDamageTo((Card) def, pwatkr.getNetCombatDamage(), pwatkr, true);
}
}
if ((!onlyIfLethal && damageToPW > 0) || damageToPW >= ((Card) def).getCounters(CounterType.LOYALTY)) {
threatenedPWs.add((Card) def);
}
}
}
}
CardCollection pwsWithChumpBlocks = new CardCollection();
CardCollection chosenChumpBlockers = new CardCollection();
CardCollection chumpPWDefenders = CardLists.filter(new CardCollection(this.blockersLeft), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.evaluateCreature(card) <= (card.isToken() ? evalThresholdToken
: evalThresholdNonToken);
}
});
CardLists.sortByPowerAsc(chumpPWDefenders);
if (!chumpPWDefenders.isEmpty()) {
for (final Card attacker : attackers) {
GameEntity def = combat.getDefenderByAttacker(attacker);
if (def instanceof Card && threatenedPWs.contains((Card) def)) {
if (attacker.hasKeyword("Trample")) {
// don't bother trying to chump a trampling creature
continue;
}
if (!combat.getBlockers(attacker).isEmpty()) {
// already blocked by something, no need to chump
continue;
}
Card blockerDecided = null;
for (final Card blocker : chumpPWDefenders) {
if (CombatUtil.canBlock(attacker, blocker, combat)) {
combat.addBlocker(attacker, blocker);
pwsWithChumpBlocks.add((Card) combat.getDefenderByAttacker(attacker));
chosenChumpBlockers.add(blocker);
blockerDecided = blocker;
blockersLeft.remove(blocker);
break;
}
}
chumpPWDefenders.remove(blockerDecided);
}
}
// check to see if we managed to cover all the blockers of the planeswalker; if not, bail
for (final Card pw : pwsWithChumpBlocks) {
CardCollection pwAttackers = combat.getAttackersOf(pw);
CardCollection pwDefenders = new CardCollection();
boolean isFullyBlocked = true;
if (!pwAttackers.isEmpty()) {
int damageToPW = 0;
for (Card pwAtk : pwAttackers) {
if (!combat.getBlockers(pwAtk).isEmpty()) {
pwDefenders.addAll(combat.getBlockers(pwAtk));
} else {
isFullyBlocked = false;
damageToPW += ComputerUtilCombat.predictDamageTo((Card) pw, pwAtk.getNetCombatDamage(), pwAtk, true);
}
}
if (!isFullyBlocked && damageToPW >= pw.getCounters(CounterType.LOYALTY)) {
for (Card chump : pwDefenders) {
if (chosenChumpBlockers.contains(chump)) {
combat.removeFromCombat(chump);
}
}
}
}
}
}
}
}
private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) { private void clearBlockers(final Combat combat, final List<Card> possibleBlockers) {
final List<Card> oldBlockers = combat.getAllBlockers(); final List<Card> oldBlockers = combat.getAllBlockers();
@@ -824,7 +984,7 @@ public class AiBlockController {
List<Card> chumpBlockers; List<Card> chumpBlockers;
diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade diff = (ai.getLife() * 2) - 5; // This is the minimal gain for an unnecessary trade
if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getProperty(AiProps.PLAY_AGGRO).equals("true")) { if (ai.getController().isAI() && diff > 0 && ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.PLAY_AGGRO)) {
diff = 0; diff = 0;
} }
@@ -856,9 +1016,8 @@ public class AiBlockController {
// When the AI holds some Fog effect, don't bother about lifeInDanger // When the AI holds some Fog effect, don't bother about lifeInDanger
if (!ComputerUtil.hasAFogEffect(ai)) { if (!ComputerUtil.hasAFogEffect(ai)) {
lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat); lifeInDanger = ComputerUtilCombat.lifeInDanger(ai, combat);
if (lifeInDanger) { makeTradeBlocks(combat); // choose necessary trade blocks
makeTradeBlocks(combat); // choose necessary trade blocks
}
// if life is still in danger // if life is still in danger
if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) { if (lifeInDanger && ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeChumpBlocks(combat); // choose necessary chump blocks makeChumpBlocks(combat); // choose necessary chump blocks
@@ -956,6 +1115,18 @@ public class AiBlockController {
} }
} }
} }
// check to see if it's possible to defend a Planeswalker under attack with a chump block,
// unless life is low enough to be more worried about saving preserving the life total
if (ai.getController().isAI() && !ComputerUtilCombat.lifeInDanger(ai, combat)) {
makeChumpBlocksToSavePW(combat);
}
// if there are still blockers left, see if it's possible to block Menace creatures with
// non-lethal blockers that won't kill the attacker but won't die to it as well
makeGangNonLethalBlocks(combat);
//Check for validity of blocks in case something slipped through //Check for validity of blocks in case something slipped through
for (Card attacker : attackers) { for (Card attacker : attackers) {
if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) { if (!CombatUtil.canAttackerBeBlockedWithAmount(attacker, combat.getBlockers(attacker).size(), combat)) {
@@ -1056,4 +1227,81 @@ public class AiBlockController {
return first; return first;
} }
private boolean wouldLikeToRandomlyTrade(Card attacker, Card blocker, Combat combat) {
// Determines if the AI would like to randomly trade its blocker for the attacker in given combat
boolean enableRandomTrades = false;
boolean randomTradeIfBehindOnBoard = false;
boolean randomTradeIfCreatInHand = false;
int chanceToTradeToSaveWalker = 0;
int chanceToTradeDownToSaveWalker = 0;
int minRandomTradeChance = 0;
int maxRandomTradeChance = 0;
int maxCreatDiff = 0;
int maxCreatDiffWithRepl = 0;
int aiCreatureCount = 0;
int oppCreatureCount = 0;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
enableRandomTrades = aic.getBooleanProperty(AiProps.ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK);
randomTradeIfBehindOnBoard = aic.getBooleanProperty(AiProps.RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS);
randomTradeIfCreatInHand = aic.getBooleanProperty(AiProps.ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT);
minRandomTradeChance = aic.getIntProperty(AiProps.MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
maxRandomTradeChance = aic.getIntProperty(AiProps.MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK);
maxCreatDiff = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE);
maxCreatDiffWithRepl = aic.getIntProperty(AiProps.MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL);
chanceToTradeToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER);
chanceToTradeDownToSaveWalker = aic.getIntProperty(AiProps.CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER);
}
if (!enableRandomTrades) {
return false;
}
aiCreatureCount = ComputerUtil.countUsefulCreatures(ai);
if (!attackersLeft.isEmpty()) {
oppCreatureCount = ComputerUtil.countUsefulCreatures(attackersLeft.get(0).getController());
}
if (attacker.getOwner().equals(ai) && "6".equals(attacker.getSVar("SacMe"))) {
// Temporarily controlled object - don't trade with it
// TODO: find a more reliable way to figure out that control will be reestablished next turn
return false;
}
int numSteps = ai.getStartingLife() - 5; // e.g. 15 steps between 5 life and 20 life
float chanceStep = (maxRandomTradeChance - minRandomTradeChance) / numSteps;
int chance = (int)Math.max(minRandomTradeChance, (maxRandomTradeChance - (Math.max(5, ai.getLife() - 5)) * chanceStep));
if (chance > maxRandomTradeChance) {
chance = maxRandomTradeChance;
}
int evalAtk = ComputerUtilCard.evaluateCreature(attacker, true, false);
int evalBlk = ComputerUtilCard.evaluateCreature(blocker, true, false);
if (blocker.isFaceDown() && blocker.getState(CardStateName.Original).getType().isCreature()) {
// if the blocker is a face-down creature (e.g. cast via Morph, Manifest), evaluate it
// in relation to the original state, not to the Morph state
evalBlk = ComputerUtilCard.evaluateCreature(Card.fromPaperCard(blocker.getPaperCard(), ai), false, true);
}
int chanceToSavePW = chanceToTradeDownToSaveWalker > 0 && evalAtk + 1 < evalBlk ? chanceToTradeDownToSaveWalker : chanceToTradeToSaveWalker;
boolean powerParityOrHigher = blocker.getNetPower() <= attacker.getNetPower();
boolean creatureParityOrAllowedDiff = aiCreatureCount
+ (randomTradeIfBehindOnBoard ? maxCreatDiff : 0) >= oppCreatureCount;
boolean wantToTradeWithCreatInHand = randomTradeIfCreatInHand
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).isEmpty()
&& aiCreatureCount + maxCreatDiffWithRepl >= oppCreatureCount;
boolean wantToSavePlaneswalker = MyRandom.percentTrue(chanceToSavePW)
&& combat.getDefenderByAttacker(attacker) instanceof Card
&& ((Card) combat.getDefenderByAttacker(attacker)).isPlaneswalker();
boolean wantToTradeDownToSavePW = chanceToTradeDownToSaveWalker > 0;
if (((evalBlk <= evalAtk + 1) || (wantToSavePlaneswalker && wantToTradeDownToSavePW)) // "1" accounts for tapped.
&& powerParityOrHigher
&& (creatureParityOrAllowedDiff || wantToTradeWithCreatInHand)
&& (MyRandom.percentTrue(chance) || wantToSavePlaneswalker)) {
return true;
}
return false;
}
} }

View File

@@ -41,17 +41,23 @@ import java.util.Set;
public class AiCardMemory { public class AiCardMemory {
private final Set<Card> memMandatoryAttackers; private final Set<Card> memMandatoryAttackers;
private final Set<Card> memTrickAttackers;
private final Set<Card> memHeldManaSources; private final Set<Card> memHeldManaSources;
private final Set<Card> memHeldManaSourcesForCombat;
private final Set<Card> memAttachedThisTurn; private final Set<Card> memAttachedThisTurn;
private final Set<Card> memAnimatedThisTurn; private final Set<Card> memAnimatedThisTurn;
private final Set<Card> memBouncedThisTurn; private final Set<Card> memBouncedThisTurn;
private final Set<Card> memActivatedThisTurn;
public AiCardMemory() { public AiCardMemory() {
this.memMandatoryAttackers = new HashSet<>(); this.memMandatoryAttackers = new HashSet<>();
this.memHeldManaSources = new HashSet<>(); this.memHeldManaSources = new HashSet<>();
this.memHeldManaSourcesForCombat = new HashSet<>();
this.memAttachedThisTurn = new HashSet<>(); this.memAttachedThisTurn = new HashSet<>();
this.memAnimatedThisTurn = new HashSet<>(); this.memAnimatedThisTurn = new HashSet<>();
this.memBouncedThisTurn = new HashSet<>(); this.memBouncedThisTurn = new HashSet<>();
this.memActivatedThisTurn = new HashSet<>();
this.memTrickAttackers = new HashSet<>();
} }
/** /**
@@ -61,10 +67,13 @@ public class AiCardMemory {
*/ */
public enum MemorySet { public enum MemorySet {
MANDATORY_ATTACKERS, MANDATORY_ATTACKERS,
HELD_MANA_SOURCES, TRICK_ATTACKERS,
HELD_MANA_SOURCES_FOR_MAIN2,
HELD_MANA_SOURCES_FOR_DECLBLK,
ATTACHED_THIS_TURN, ATTACHED_THIS_TURN,
ANIMATED_THIS_TURN, ANIMATED_THIS_TURN,
BOUNCED_THIS_TURN, BOUNCED_THIS_TURN,
ACTIVATED_THIS_TURN,
//REVEALED_CARDS // stub, not linked to AI code yet //REVEALED_CARDS // stub, not linked to AI code yet
} }
@@ -72,14 +81,20 @@ public class AiCardMemory {
switch (set) { switch (set) {
case MANDATORY_ATTACKERS: case MANDATORY_ATTACKERS:
return memMandatoryAttackers; return memMandatoryAttackers;
case HELD_MANA_SOURCES: case TRICK_ATTACKERS:
return memTrickAttackers;
case HELD_MANA_SOURCES_FOR_MAIN2:
return memHeldManaSources; return memHeldManaSources;
case HELD_MANA_SOURCES_FOR_DECLBLK:
return memHeldManaSourcesForCombat;
case ATTACHED_THIS_TURN: case ATTACHED_THIS_TURN:
return memAttachedThisTurn; return memAttachedThisTurn;
case ANIMATED_THIS_TURN: case ANIMATED_THIS_TURN:
return memAnimatedThisTurn; return memAnimatedThisTurn;
case BOUNCED_THIS_TURN: case BOUNCED_THIS_TURN:
return memBouncedThisTurn; return memBouncedThisTurn;
case ACTIVATED_THIS_TURN:
return memActivatedThisTurn;
//case REVEALED_CARDS: //case REVEALED_CARDS:
// return memRevealedCards; // return memRevealedCards;
default: default:
@@ -249,10 +264,13 @@ public class AiCardMemory {
*/ */
public void clearAllRemembered() { public void clearAllRemembered() {
clearMemorySet(MemorySet.MANDATORY_ATTACKERS); clearMemorySet(MemorySet.MANDATORY_ATTACKERS);
clearMemorySet(MemorySet.HELD_MANA_SOURCES); clearMemorySet(MemorySet.TRICK_ATTACKERS);
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
clearMemorySet(MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
clearMemorySet(MemorySet.ATTACHED_THIS_TURN); clearMemorySet(MemorySet.ATTACHED_THIS_TURN);
clearMemorySet(MemorySet.ANIMATED_THIS_TURN); clearMemorySet(MemorySet.ANIMATED_THIS_TURN);
clearMemorySet(MemorySet.BOUNCED_THIS_TURN); clearMemorySet(MemorySet.BOUNCED_THIS_TURN);
clearMemorySet(MemorySet.ACTIVATED_THIS_TURN);
} }
// Static functions to simplify access to AI card memory of a given AI player. // Static functions to simplify access to AI card memory of a given AI player.
@@ -265,6 +283,9 @@ public class AiCardMemory {
public static boolean isRememberedCard(Player ai, Card c, MemorySet set) { public static boolean isRememberedCard(Player ai, Card c, MemorySet set) {
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set); return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCard(c, set);
} }
public static boolean isRememberedCardByName(Player ai, String name, MemorySet set) {
return ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().isRememberedCardByName(name, set);
}
public static void clearMemorySet(Player ai, MemorySet set) { public static void clearMemorySet(Player ai, MemorySet set) {
((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set); ((PlayerControllerAi)ai.getController()).getAi().getCardMemory().clearMemorySet(set);
} }

View File

@@ -17,14 +17,6 @@
*/ */
package forge.ai; package forge.ai;
import java.security.InvalidParameterException;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import com.esotericsoftware.minlog.Log; import com.esotericsoftware.minlog.Log;
import com.google.common.base.Function; import com.google.common.base.Function;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
@@ -32,52 +24,31 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.ability.ChangeZoneAi; import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.ExploreAi;
import forge.ai.simulation.SpellAbilityPicker; import forge.ai.simulation.SpellAbilityPicker;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.deck.CardPool; import forge.deck.CardPool;
import forge.deck.Deck; import forge.deck.Deck;
import forge.deck.DeckSection; import forge.deck.DeckSection;
import forge.game.CardTraitBase; import forge.game.*;
import forge.game.CardTraitPredicates;
import forge.game.Direction;
import forge.game.Game;
import forge.game.GameEntity;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.SpellApiBased; import forge.game.ability.SpellApiBased;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.*;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostRemoveCounter;
import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.replacement.ReplaceMoved; import forge.game.replacement.ReplaceMoved;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.AbilityManaPart; import forge.game.spellability.*;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.OptionalCost;
import forge.game.spellability.Spell;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition;
import forge.game.spellability.SpellAbilityPredicates;
import forge.game.spellability.SpellPermanent;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.trigger.WrappedAbility; import forge.game.trigger.WrappedAbility;
@@ -88,6 +59,10 @@ import forge.util.Expressions;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import java.security.InvalidParameterException;
import java.util.*;
import java.util.Map.Entry;
/** /**
* <p> * <p>
* AiController class. * AiController class.
@@ -605,11 +580,32 @@ public class AiController {
return null; return null;
} }
public void reserveManaSourcesForMain2(SpellAbility sa) { public void reserveManaSources(SpellAbility sa) {
reserveManaSources(sa, PhaseType.MAIN2);
}
public void reserveManaSources(SpellAbility sa, PhaseType phaseType) {
ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0); ManaCostBeingPaid cost = ComputerUtilMana.calculateManaCost(sa, true, 0);
CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player); CardCollection manaSources = ComputerUtilMana.getManaSourcesToPayCost(cost, sa, player);
AiCardMemory.MemorySet memSet;
switch (phaseType) {
case MAIN2:
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
break;
case COMBAT_DECLARE_BLOCKERS:
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK;
break;
default:
System.out.println("Warning: unsupported mana reservation phase specified for reserveManaSources: "
+ phaseType.name() + ", reserving until Main 2 instead. Consider adding support for the phase if needed.");
memSet = AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2;
break;
}
for (Card c : manaSources) { for (Card c : manaSources) {
AiCardMemory.rememberCard(player, c, AiCardMemory.MemorySet.HELD_MANA_SOURCES); AiCardMemory.rememberCard(player, c, memSet);
} }
} }
@@ -724,12 +720,31 @@ public class AiController {
int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC(); int b1 = b.getPayCosts() == null ? 0 : b.getPayCosts().getTotalMana().getCMC();
// deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True // deprioritize planar die roll marked with AIRollPlanarDieParams:LowPriority$ True
if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { if (ApiType.RollPlanarDice == a.getApi() && a.getHostCard() != null && a.getHostCard().hasSVar("AIRollPlanarDieParams") && a.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
return 1; return 1;
} else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) { } else if (ApiType.RollPlanarDice == b.getApi() && b.getHostCard() != null && b.getHostCard().hasSVar("AIRollPlanarDieParams") && b.getHostCard().getSVar("AIRollPlanarDieParams").toLowerCase().matches(".*lowpriority\\$\\s*true.*")) {
return -1; return -1;
} }
// deprioritize pump spells with pure energy cost (can be activated last,
// since energy is generally scarce, plus can benefit e.g. Electrostatic Pummeler)
int a2 = 0, b2 = 0;
if (a.getApi() == ApiType.Pump && a.getPayCosts() != null && a.getPayCosts().getCostEnergy() != null) {
if (a.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
a2 = a.getPayCosts().getCostEnergy().convertAmount();
}
}
if (b.getApi() == ApiType.Pump && b.getPayCosts() != null && b.getPayCosts().getCostEnergy() != null) {
if (b.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
b2 = b.getPayCosts().getCostEnergy().convertAmount();
}
}
if (a2 == 0 && b2 > 0) {
return -1;
} else if (b2 == 0 && a2 > 0) {
return 1;
}
// cast 0 mana cost spells first (might be a Mox) // cast 0 mana cost spells first (might be a Mox)
if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) { if (a1 == 0 && b1 > 0 && ApiType.Mana != a.getApi()) {
return -1; return -1;
@@ -737,9 +752,9 @@ public class AiController {
return 1; return 1;
} }
if (a.getHostCard().hasSVar("FreeSpellAI")) { if (a.getHostCard() != null && a.getHostCard().hasSVar("FreeSpellAI")) {
return -1; return -1;
} else if (b.getHostCard().hasSVar("FreeSpellAI")) { } else if (b.getHostCard() != null && b.getHostCard().hasSVar("FreeSpellAI")) {
return 1; return 1;
} }
@@ -752,36 +767,48 @@ public class AiController {
private int getSpellAbilityPriority(SpellAbility sa) { private int getSpellAbilityPriority(SpellAbility sa) {
int p = 0; int p = 0;
Card source = sa.getHostCard(); Card source = sa.getHostCard();
final Player ai = source.getController(); final Player ai = source == null ? sa.getActivatingPlayer() : source.getController();
if (ai == null) {
System.err.println("Error: couldn't figure out the activating player and host card for SA: " + sa);
return 0;
}
final boolean noCreatures = ai.getCreaturesInPlay().isEmpty(); final boolean noCreatures = ai.getCreaturesInPlay().isEmpty();
// puts creatures in front of spells
if (source.isCreature()) { if (source != null) {
p += 1; // puts creatures in front of spells
} if (source.isCreature()) {
// don't play equipments before having any creatures p += 1;
if (source.isEquipment() && noCreatures) { }
p -= 9; // don't play equipments before having any creatures
if (source.isEquipment() && noCreatures) {
p -= 9;
}
// 1. increase chance of using Surge effects
// 2. non-surged versions are usually inefficient
if (source.getOracleText().contains("surge cost") && !sa.isSurged()) {
p -= 9;
}
// move snap-casted spells to front
if (source.isInZone(ZoneType.Graveyard)) {
if (sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) {
p += 50;
}
}
// artifacts and enchantments with effects that do not stack
if ("True".equals(source.getSVar("NonStackingEffect")) && ai.isCardInPlay(source.getName())) {
p -= 9;
}
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
p -= (((PlayerControllerAi) ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
}
} }
// use Surge and Prowl costs when able to // use Surge and Prowl costs when able to
if (sa.isSurged() || if (sa.isSurged() ||
(sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) { (sa.getRestrictions().getProwlTypes() != null && !sa.getRestrictions().getProwlTypes().isEmpty())) {
p += 9; p += 9;
} }
// 1. increase chance of using Surge effects
// 2. non-surged versions are usually inefficient
if (sa.getHostCard().getOracleText().contains("surge cost") && !sa.isSurged()) {
p -= 9;
}
// move snap-casted spells to front
if (source.isInZone(ZoneType.Graveyard)) {
if(sa.getMayPlay() != null && source.mayPlay(sa.getMayPlay()) != null) {
p += 50;
}
}
// artifacts and enchantments with effects that do not stack
if ("True".equals(source.getSVar("NonStackingEffect")) && ai.isCardInPlay(source.getName())) {
p -= 9;
}
// sort planeswalker abilities with most costly first // sort planeswalker abilities with most costly first
if (sa.getRestrictions().isPwAbility()) { if (sa.getRestrictions().isPwAbility()) {
final CostPart cost = sa.getPayCosts().getCostParts().get(0); final CostPart cost = sa.getPayCosts().getCostParts().get(0);
@@ -801,11 +828,6 @@ public class AiController {
p -= 9; p -= 9;
} }
// if the profile specifies it, deprioritize Storm spells in an attempt to build up storm count
if (source.hasKeyword("Storm") && ai.getController() instanceof PlayerControllerAi) {
p -= (((PlayerControllerAi)ai.getController()).getAi().getIntProperty(AiProps.PRIORITY_REDUCTION_FOR_STORM_SPELLS));
}
// try to cast mana ritual spells before casting spells to maximize potential mana // try to cast mana ritual spells before casting spells to maximize potential mana
if ("ManaRitual".equals(sa.getParam("AILogic"))) { if ("ManaRitual".equals(sa.getParam("AILogic"))) {
p += 9; p += 9;
@@ -840,6 +862,8 @@ public class AiController {
sourceCard = sa.getHostCard(); sourceCard = sa.getHostCard();
if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) { if ("Always".equals(sa.getParam("AILogic")) && !validCards.isEmpty()) {
min = 1; min = 1;
} else if ("VolrathsShapeshifter".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.VolrathsShapeshifter.targetBestCreature(player, sa);
} }
} }
@@ -858,6 +882,9 @@ public class AiController {
} }
if (prefCard == null) { if (prefCard == null) {
prefCard = ComputerUtil.getCardPreference(player, sourceCard, "DiscardCost", validCards); prefCard = ComputerUtil.getCardPreference(player, sourceCard, "DiscardCost", validCards);
if (prefCard != null && prefCard.hasSVar("DoNotDiscardIfAble")) {
prefCard = null;
}
} }
if (prefCard != null) { if (prefCard != null) {
discardList.add(prefCard); discardList.add(prefCard);
@@ -894,13 +921,52 @@ public class AiController {
if (numLandsInHand > 0) { if (numLandsInHand > 0) {
numLandsAvailable++; numLandsAvailable++;
} }
//Discard unplayable card //Discard unplayable card
if (validCards.get(0).getCMC() > numLandsAvailable) { boolean discardedUnplayable = false;
discardList.add(validCards.get(0)); for (int j = 0; j < validCards.size(); j++) {
validCards.remove(validCards.get(0)); if (validCards.get(j).getCMC() > numLandsAvailable && !validCards.get(j).hasSVar("DoNotDiscardIfAble")) {
discardList.add(validCards.get(j));
validCards.remove(validCards.get(j));
discardedUnplayable = true;
break;
} else if (validCards.get(j).getCMC() <= numLandsAvailable) {
// cut short to avoid looping over cards which are guaranteed not to fit the criteria
break;
}
} }
else { //Discard worst card
if (!discardedUnplayable) {
// discard worst card
Card worst = ComputerUtilCard.getWorstAI(validCards); Card worst = ComputerUtilCard.getWorstAI(validCards);
if (worst == null) {
// there were only instants and sorceries, and maybe cards that are not good to discard, so look
// for more discard options
worst = ComputerUtilCard.getCheapestSpellAI(validCards);
}
if (worst == null && !validCards.isEmpty()) {
// still nothing chosen, so choose the first thing that works, trying not to make DoNotDiscardIfAble
// discards
for (Card c : validCards) {
if (!c.hasSVar("DoNotDiscardIfAble")) {
worst = c;
break;
}
}
// Only DoNotDiscardIfAble cards? If we have a duplicate for something, discard it
if (worst == null) {
for (Card c : validCards) {
if (CardLists.filter(player.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c.getName())).size() > 1) {
worst = c;
break;
}
}
if (worst == null) {
// Otherwise just grab a random card and discard it
worst = Aggregates.random(validCards);
}
}
}
discardList.add(worst); discardList.add(worst);
validCards.remove(worst); validCards.remove(worst);
} }
@@ -1059,6 +1125,20 @@ public class AiController {
} }
CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player); CardCollection landsWannaPlay = ComputerUtilAbility.getAvailableLandsToPlay(game, player);
CardCollection playBeforeLand = CardLists.filter(player.getCardsIn(ZoneType.Hand), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return "true".equalsIgnoreCase(card.getSVar("PlayBeforeLandDrop"));
}
});
if (!playBeforeLand.isEmpty()) {
SpellAbility wantToPlayBeforeLand = chooseSpellAbilityToPlayFromList(ComputerUtilAbility.getSpellAbilities(playBeforeLand, player), false);
if (wantToPlayBeforeLand != null) {
return singleSpellAbilityList(wantToPlayBeforeLand);
}
}
if (landsWannaPlay != null) { if (landsWannaPlay != null) {
landsWannaPlay = filterLandsToPlay(landsWannaPlay); landsWannaPlay = filterLandsToPlay(landsWannaPlay);
Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi); Log.debug("Computer " + game.getPhaseHandler().getPhase().nameForUi);
@@ -1066,10 +1146,12 @@ public class AiController {
Card land = chooseBestLandToPlay(landsWannaPlay); Card land = chooseBestLandToPlay(landsWannaPlay);
if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife() if (ComputerUtil.getDamageFromETB(player, land) < player.getLife() || !player.canLoseLife()
|| player.cantLoseForZeroOrLessLife() ) { || player.cantLoseForZeroOrLessLife() ) {
game.PLAY_LAND_SURROGATE.setHostCard(land); if (!game.getPhaseHandler().is(PhaseType.MAIN1) || !isSafeToHoldLandDropForMain2(land)) {
final List<SpellAbility> abilities = Lists.newArrayList(); game.PLAY_LAND_SURROGATE.setHostCard(land);
abilities.add(game.PLAY_LAND_SURROGATE); final List<SpellAbility> abilities = Lists.newArrayList();
return abilities; abilities.add(game.PLAY_LAND_SURROGATE);
return abilities;
}
} }
} }
} }
@@ -1077,6 +1159,103 @@ public class AiController {
return singleSpellAbilityList(getSpellAbilityToPlay()); return singleSpellAbilityList(getSpellAbilityToPlay());
} }
private boolean isSafeToHoldLandDropForMain2(Card landToPlay) {
if (!MyRandom.percentTrue(getIntProperty(AiProps.HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED))) {
// check against the chance specified in the profile
return false;
}
if (game.getPhaseHandler().getTurn() <= 2) {
// too obvious when doing it on the very first turn of the game
return false;
}
CardCollection inHand = CardLists.filter(player.getCardsIn(ZoneType.Hand),
Predicates.not(CardPredicates.Presets.LANDS));
CardCollectionView otb = player.getCardsIn(ZoneType.Battlefield);
// TODO: improve the detection of taplands
boolean isTapLand = false;
for (ReplacementEffect repl : landToPlay.getReplacementEffects()) {
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
isTapLand = true;
}
}
int totalCMCInHand = Aggregates.sum(inHand, CardPredicates.Accessors.fnGetCmc);
int minCMCInHand = Aggregates.min(inHand, CardPredicates.Accessors.fnGetCmc);
int predictedMana = ComputerUtilMana.getAvailableManaEstimate(player, true);
boolean canCastWithLandDrop = (predictedMana + 1 >= minCMCInHand) && !isTapLand;
boolean cantCastAnythingNow = predictedMana < minCMCInHand;
boolean hasRelevantAbsOTB = !CardLists.filter(otb, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
boolean isTapLand = false;
for (ReplacementEffect repl : card.getReplacementEffects()) {
// TODO: improve the detection of taplands
if (repl.getParamOrDefault("Description", "").equals("CARDNAME enters the battlefield tapped.")) {
isTapLand = true;
}
}
for (SpellAbility sa : card.getSpellAbilities()) {
if (sa.getPayCosts() != null && sa.isAbility()
&& sa.getPayCosts().getCostMana() != null
&& sa.getPayCosts().getCostMana().getMana().getCMC() > 0
&& (!sa.getPayCosts().hasTapCost() || !isTapLand)
&& (!sa.hasParam("ActivationZone") || sa.getParam("ActivationZone").contains("Battlefield"))) {
return true;
}
}
return false;
}
}).isEmpty();
boolean hasLandBasedEffect = !CardLists.filter(otb, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
for (Trigger t : card.getTriggers()) {
Map<String, String> params = t.getMapParams();
if ("ChangesZone".equals(params.get("Mode"))
&& params.containsKey("ValidCard")
&& !params.get("ValidCard").contains("nonLand")
&& ((params.get("ValidCard").contains("Land")) || (params.get("ValidCard").contains("Permanent")))
&& "Battlefield".equals(params.get("Destination"))) {
// Landfall and other similar triggers
return true;
}
}
for (String sv : card.getSVars().keySet()) {
String varValue = card.getSVar(sv);
if (varValue.startsWith("Count$Valid") || sv.equals("BuffedBy")) {
if (varValue.contains("Land") || varValue.contains("Plains") || varValue.contains("Forest")
|| varValue.contains("Mountain") || varValue.contains("Island") || varValue.contains("Swamp")
|| varValue.contains("Wastes")) {
// In presence of various cards that get buffs like "equal to the number of lands you control",
// safer for our AI model to just play the land earlier rather than make a blunder
return true;
}
}
}
return false;
}
}).isEmpty();
// TODO: add prediction for effects that will untap a tapland as it enters the battlefield
if (!canCastWithLandDrop && cantCastAnythingNow && !hasLandBasedEffect && (!hasRelevantAbsOTB || isTapLand)) {
// Hopefully there's not much to do with the extra mana immediately, can wait for Main 2
return true;
}
if ((predictedMana <= totalCMCInHand && canCastWithLandDrop) || (hasRelevantAbsOTB && !isTapLand) || hasLandBasedEffect) {
// Might need an extra land to cast something, or for some kind of an ETB ability with a cost or an
// alternative cost (if we cast it in Main 1), or to use an activated ability on the battlefield
return false;
}
return true;
}
private final SpellAbility getSpellAbilityToPlay() { private final SpellAbility getSpellAbilityToPlay() {
// if top of stack is owned by me // if top of stack is owned by me
if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) { if (!game.getStack().isEmpty() && game.getStack().peekAbility().getActivatingPlayer().equals(player)) {
@@ -1306,7 +1485,7 @@ public class AiController {
+ MyRandom.getRandom().nextInt(3); + MyRandom.getRandom().nextInt(3);
return Math.max(remaining, min) / 2; return Math.max(remaining, min) / 2;
} else if ("LowestLoseLife".equals(logic)) { } else if ("LowestLoseLife".equals(logic)) {
return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, player.getOpponent().getLife())) + 1; return MyRandom.getRandom().nextInt(Math.min(player.getLife() / 3, ComputerUtil.getOpponentFor(player).getLife())) + 1;
} else if ("HighestGetCounter".equals(logic)) { } else if ("HighestGetCounter".equals(logic)) {
return MyRandom.getRandom().nextInt(3); return MyRandom.getRandom().nextInt(3);
} else if (source.hasSVar("EnergyToPay")) { } else if (source.hasSVar("EnergyToPay")) {
@@ -1428,6 +1607,24 @@ public class AiController {
} else { } else {
break; break;
} }
// Special case for Bow to My Command which simulates a complex tap cost via ChooseCard
// TODO: consider enhancing support for tapXType<Any/...> in UnlessCost to get rid of this hack
if ("BowToMyCommand".equals(sa.getParam("AILogic"))) {
if (!sa.getHostCard().getZone().is(ZoneType.Command)) {
// Make sure that other opponents do not tap for an already abandoned scheme
result.clear();
break;
}
int totPower = 0;
for (Card p : result) {
totPower += p.getNetPower();
}
if (totPower >= 8) {
break;
}
}
} }
} }
@@ -1553,7 +1750,11 @@ public class AiController {
if (useSimulation) { if (useSimulation) {
return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); return simPicker.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
} }
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); if (sa.getApi() == ApiType.Explore) {
return ExploreAi.shouldPutInGraveyard(fetchList, decider);
} else {
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
}
} }
public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) { public List<SpellAbility> orderPlaySa(List<SpellAbility> activePlayerSAs) {

View File

@@ -469,7 +469,7 @@ public class AiCostDecision extends CostDecisionMakerBase {
CardCollectionView totap; CardCollectionView totap;
if (isVehicle) { if (isVehicle) {
totalP = type.split("withTotalPowerGE")[1]; totalP = type.split("withTotalPowerGE")[1];
type = type.replace("+withTotalPowerGE" + totalP, ""); type = TextUtil.fastReplace(type, "+withTotalPowerGE", "");
totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), tapped); totap = ComputerUtil.chooseTapTypeAccumulatePower(player, type, ability, !cost.canTapSource, Integer.parseInt(totalP), tapped);
} else { } else {
totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped); totap = ComputerUtil.chooseTapType(player, type, source, !cost.canTapSource, c, tapped);

View File

@@ -21,6 +21,7 @@ import forge.LobbyPlayer;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.FileUtil; import forge.util.FileUtil;
import forge.util.TextUtil;
import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.ArrayUtils;
import java.io.File; import java.io.File;
@@ -52,7 +53,7 @@ public class AiProfileUtil {
* @return the full relative path and file name for the given profile. * @return the full relative path and file name for the given profile.
*/ */
private static String buildFileName(final String profileName) { private static String buildFileName(final String profileName) {
return String.format("%s/%s%s", AI_PROFILE_DIR, profileName, AI_PROFILE_EXT); return TextUtil.concatNoSpace(AI_PROFILE_DIR, "/", profileName, AI_PROFILE_EXT);
} }
/** /**

View File

@@ -29,20 +29,45 @@ public enum AiProps { /** */
DEFAULT_PLANAR_DIE_ROLL_CHANCE ("50"), /** */ DEFAULT_PLANAR_DIE_ROLL_CHANCE ("50"), /** */
MULLIGAN_THRESHOLD ("5"), /** */ MULLIGAN_THRESHOLD ("5"), /** */
PLANAR_DIE_ROLL_HESITATION_CHANCE ("10"), PLANAR_DIE_ROLL_HESITATION_CHANCE ("10"),
HOLD_LAND_DROP_FOR_MAIN2_IF_UNUSED ("0"), /** */
CHEAT_WITH_MANA_ON_SHUFFLE ("false"), CHEAT_WITH_MANA_ON_SHUFFLE ("false"),
MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"), MOVE_EQUIPMENT_TO_BETTER_CREATURES ("from_useless_only"),
MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"), MOVE_EQUIPMENT_CREATURE_EVAL_THRESHOLD ("60"),
PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"), PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS ("true"),
PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */ PREDICT_SPELLS_FOR_MAIN2 ("true"), /** */
RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */ RESERVE_MANA_FOR_MAIN2_CHANCE ("0"), /** */
PLAY_AGGRO ("false"), /** */ PLAY_AGGRO ("false"),
MIN_SPELL_CMC_TO_COUNTER ("0"), CHANCE_TO_ATTACK_INTO_TRADE ("40"), /** */
RANDOMLY_ATKTRADE_ONLY_ON_LOWER_LIFE_PRESSURE ("true"), /** */
ATTACK_INTO_TRADE_WHEN_TAPPED_OUT ("false"), /** */
CHANCE_TO_ATKTRADE_WHEN_OPP_HAS_MANA ("0"), /** */
TRY_TO_AVOID_ATTACKING_INTO_CERTAIN_BLOCK ("true"), /** */
TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK ("false"), /** */
CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK ("30"), /** */
ENABLE_RANDOM_FAVORABLE_TRADES_ON_BLOCK ("true"), /** */
RANDOMLY_TRADE_EVEN_WHEN_HAVE_LESS_CREATS ("false"), /** */
MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE ("1"), /** */
ALSO_TRADE_WHEN_HAVE_A_REPLACEMENT_CREAT ("true"), /** */
MAX_DIFF_IN_CREATURE_COUNT_TO_TRADE_WITH_REPL ("1"), /** */
MIN_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK ("30"), /** */
MAX_CHANCE_TO_RANDOMLY_TRADE_ON_BLOCK ("70"), /** */
CHANCE_TO_TRADE_TO_SAVE_PLANESWALKER ("70"), /** */
CHANCE_TO_TRADE_DOWN_TO_SAVE_PLANESWALKER ("0"), /** */
THRESHOLD_TOKEN_CHUMP_TO_SAVE_PLANESWALKER ("135"), /** */
THRESHOLD_NONTOKEN_CHUMP_TO_SAVE_PLANESWALKER ("110"), /** */
CHUMP_TO_SAVE_PLANESWALKER_ONLY_ON_LETHAL ("true"), /** */
MIN_SPELL_CMC_TO_COUNTER ("0"), /** */
CHANCE_TO_COUNTER_CMC_1 ("50"), /** */
CHANCE_TO_COUNTER_CMC_2 ("75"), /** */
CHANCE_TO_COUNTER_CMC_3 ("100"), /** */
ALWAYS_COUNTER_OTHER_COUNTERSPELLS ("true"), /** */ ALWAYS_COUNTER_OTHER_COUNTERSPELLS ("true"), /** */
ALWAYS_COUNTER_DAMAGE_SPELLS ("true"), /** */ ALWAYS_COUNTER_DAMAGE_SPELLS ("true"), /** */
ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS ("true"), /** */ ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS ("true"), /** */
ALWAYS_COUNTER_REMOVAL_SPELLS ("true"), /** */ ALWAYS_COUNTER_REMOVAL_SPELLS ("true"), /** */
ALWAYS_COUNTER_PUMP_SPELLS ("true"), /** */
ALWAYS_COUNTER_AURAS ("true"), /** */
ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */ ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS (""), /** */
ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("false"), /** */ ACTIVELY_DESTROY_ARTS_AND_NONAURA_ENCHS ("true"), /** */
PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */ PRIORITY_REDUCTION_FOR_STORM_SPELLS ("0"), /** */
USE_BERSERK_AGGRESSIVELY ("false"), /** */ USE_BERSERK_AGGRESSIVELY ("false"), /** */
MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */ MIN_COUNT_FOR_STORM_SPELLS ("0"), /** */
@@ -50,7 +75,31 @@ public enum AiProps { /** */
STRIPMINE_MIN_LANDS_FOR_NO_TIMING_CHECK ("3"), /** */ STRIPMINE_MIN_LANDS_FOR_NO_TIMING_CHECK ("3"), /** */
STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK ("6"), /** */ STRIPMINE_MIN_LANDS_OTB_FOR_NO_TEMPO_CHECK ("6"), /** */
STRIPMINE_MAX_LANDS_TO_ATTEMPT_MANALOCKING ("3"), /** */ STRIPMINE_MAX_LANDS_TO_ATTEMPT_MANALOCKING ("3"), /** */
STRIPMINE_HIGH_PRIORITY_ON_SKIPPED_LANDDROP ("false"); /** */ STRIPMINE_HIGH_PRIORITY_ON_SKIPPED_LANDDROP ("false"),
TOKEN_GENERATION_ABILITY_CHANCE ("80"), /** */
TOKEN_GENERATION_ALWAYS_IF_FROM_PLANESWALKER ("true"), /** */
TOKEN_GENERATION_ALWAYS_IF_OPP_ATTACKS ("true"), /** */
SCRY_NUM_LANDS_TO_STILL_NEED_MORE ("4"), /** */
SCRY_NUM_LANDS_TO_NOT_NEED_MORE ("7"), /** */
SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES ("4"), /** */
SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC ("3"), /** */
SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE ("160"), /** */
SCRY_EVALTHR_CMC_THRESHOLD ("3"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM ("false"), /** */
SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF ("1"), /** */
COMBAT_ASSAULT_ATTACK_EVASION_PREDICTION ("true"), /** */
COMBAT_ATTRITION_ATTACK_EVASION_PREDICTION ("true"), /** */
CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT ("true"), /** */
CONSERVATIVE_ENERGY_PAYMENT_ONLY_DEFENSIVELY ("true"), /** */
BOUNCE_ALL_TO_HAND_CREAT_EVAL_DIFF ("200"), /** */
BOUNCE_ALL_ELSEWHERE_CREAT_EVAL_DIFF ("200"), /** */
BOUNCE_ALL_TO_HAND_NONCREAT_EVAL_DIFF ("3"), /** */
BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF ("3"), /** */
INTUITION_ALTERNATIVE_LOGIC ("false"), /** */
EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD ("2"),
EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE ("2"); /** */
// Experimental features, must be removed after extensive testing and, ideally, defaulting
// <-- There are no experimental options here -->
private final String strDefaultVal; private final String strDefaultVal;

View File

@@ -17,23 +17,9 @@
*/ */
package forge.ai; package forge.ai;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Random;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.ImmutableList; import com.google.common.collect.*;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.ai.ability.ProtectAi; import forge.ai.ability.ProtectAi;
import forge.ai.ability.TokenAi; import forge.ai.ability.TokenAi;
import forge.card.CardType; import forge.card.CardType;
@@ -46,32 +32,17 @@ import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil;
import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.*;
import forge.game.cost.CostDiscard;
import forge.game.cost.CostPart;
import forge.game.cost.CostPayment;
import forge.game.cost.CostPutCounter;
import forge.game.cost.CostSacrifice;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.replacement.ReplacementLayer; import forge.game.replacement.ReplacementLayer;
import forge.game.spellability.AbilityManaPart; import forge.game.spellability.*;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityStackInstance;
import forge.game.spellability.TargetRestrictions;
import forge.game.staticability.StaticAbility; import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger; import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
@@ -79,7 +50,11 @@ import forge.game.zone.Zone;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
/** /**
@@ -99,14 +74,24 @@ public class ComputerUtil {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
if (sa.isSpell() && !source.isCopiedSpell()) { if (sa.isSpell() && !source.isCopiedSpell()) {
if (source.getType().hasStringType("Arcane")) {
sa = AbilityUtils.addSpliceEffects(sa);
if (sa.getSplicedCards() != null && !sa.getSplicedCards().isEmpty() && ai.getController().isAI()) {
// we need to reconsider and retarget the SA after additional SAs have been added onto it via splice,
// otherwise the AI will fail to add the card to stack and that'll knock it out of the game
sa.resetTargets();
if (((PlayerControllerAi) ai.getController()).getAi().canPlaySa(sa) != AiPlayDecision.WillPlay) {
// for whatever reason the AI doesn't want to play the thing with the spliced subs anymore,
// proceeding past this point may result in an illegal play
return false;
}
}
}
source.setCastSA(sa); source.setCastSA(sa);
sa.setLastStateBattlefield(game.getLastStateBattlefield()); sa.setLastStateBattlefield(game.getLastStateBattlefield());
sa.setLastStateGraveyard(game.getLastStateGraveyard()); sa.setLastStateGraveyard(game.getLastStateGraveyard());
sa.setHostCard(game.getAction().moveToStack(source, sa)); sa.setHostCard(game.getAction().moveToStack(source, sa));
if (source.getType().hasStringType("Arcane")) {
sa = AbilityUtils.addSpliceEffects(sa);
}
} }
if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) { if (sa.getApi() == ApiType.Charm && !sa.isWrapper()) {
@@ -192,7 +177,7 @@ public class ComputerUtil {
if (unless != null && !unless.endsWith(">")) { if (unless != null && !unless.endsWith(">")) {
final int amount = AbilityUtils.calculateAmount(source, unless, sa); final int amount = AbilityUtils.calculateAmount(source, unless, sa);
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size(); final int usableManaSources = ComputerUtilMana.getAvailableManaSources(ComputerUtil.getOpponentFor(ai), true).size();
// If the Unless isn't enough, this should be less likely to be used // If the Unless isn't enough, this should be less likely to be used
if (amount > usableManaSources) { if (amount > usableManaSources) {
@@ -310,11 +295,13 @@ public class ComputerUtil {
public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) { public static Card getCardPreference(final Player ai, final Card activate, final String pref, final CardCollection typeList) {
final Game game = ai.getGame(); final Game game = ai.getGame();
String prefDef = "";
if (activate != null) { if (activate != null) {
prefDef = activate.getSVar("AIPreference");
final String[] prefGroups = activate.getSVar("AIPreference").split("\\|"); final String[] prefGroups = activate.getSVar("AIPreference").split("\\|");
for (String prefGroup : prefGroups) { for (String prefGroup : prefGroups) {
final String[] prefValid = prefGroup.trim().split("\\$"); final String[] prefValid = prefGroup.trim().split("\\$");
if (prefValid[0].equals(pref)) { if (prefValid[0].equals(pref) && !prefValid[1].startsWith("Special:")) {
final CardCollection prefList = CardLists.getValidCards(typeList, prefValid[1].split(","), activate.getController(), activate, null); final CardCollection prefList = CardLists.getValidCards(typeList, prefValid[1].split(","), activate.getController(), activate, null);
CardCollection overrideList = null; CardCollection overrideList = null;
@@ -411,6 +398,11 @@ public class ComputerUtil {
} }
} }
// Survival of the Fittest logic
if (prefDef.contains("DiscardCost$Special:SurvivalOfTheFittest")) {
return SpecialCardAi.SurvivalOfTheFittest.considerDiscardTarget(ai);
}
// Discard lands // Discard lands
final CardCollection landsInHand = CardLists.getType(typeList, "Land"); final CardCollection landsInHand = CardLists.getType(typeList, "Land");
if (!landsInHand.isEmpty()) { if (!landsInHand.isEmpty()) {
@@ -848,11 +840,11 @@ public class ComputerUtil {
continue; // Won't play ability continue; // Won't play ability
} }
if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c)) { if (!ComputerUtilCost.checkSacrificeCost(controller, abCost, c, sa)) {
continue; // Won't play ability continue; // Won't play ability
} }
if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c)) { if (!ComputerUtilCost.checkCreatureSacrificeCost(controller, abCost, c, sa)) {
continue; // Won't play ability continue; // Won't play ability
} }
} }
@@ -868,7 +860,7 @@ public class ComputerUtil {
} }
} catch (final Exception ex) { } catch (final Exception ex) {
throw new RuntimeException(String.format("There is an error in the card code for %s:%s", c.getName(), ex.getMessage()), ex); throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex);
} }
} }
} }
@@ -907,7 +899,7 @@ public class ComputerUtil {
} }
} }
} catch (final Exception ex) { } catch (final Exception ex) {
throw new RuntimeException(String.format("There is an error in the card code for %s:%s", c.getName(), ex.getMessage()), ex); throw new RuntimeException(TextUtil.concatNoSpace("There is an error in the card code for ", c.getName(), ":", ex.getMessage()), ex);
} }
} }
} }
@@ -926,7 +918,7 @@ public class ComputerUtil {
return true; return true;
} else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) { } else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) {
//Only play these main1 when the opponent has creatures (stealing and giving them haste) //Only play these main1 when the opponent has creatures (stealing and giving them haste)
if (!card.getController().getOpponent().getCreaturesInPlay().isEmpty()) { if (!ai.getOpponents().getCreaturesInPlay().isEmpty()) {
return true; return true;
} }
} else if (!card.getController().getCreaturesInPlay().isEmpty()) { } else if (!card.getController().getCreaturesInPlay().isEmpty()) {
@@ -934,6 +926,15 @@ public class ComputerUtil {
} }
} }
// try not to cast Raid creatures in main 1 if an attack is likely
if ("Count$AttackersDeclared".equals(card.getSVar("RaidTest")) && !card.hasKeyword("Haste")) {
for (Card potentialAtkr: ai.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
return false;
}
}
}
if (card.getManaCost().isZero()) { if (card.getManaCost().isZero()) {
return true; return true;
} }
@@ -973,7 +974,7 @@ public class ComputerUtil {
return true; return true;
} }
} }
if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ai.getOpponent())) { if (card.isEquipment() && buffedcard.isCreature() && CombatUtil.canAttack(buffedcard, ComputerUtil.getOpponentFor(ai))) {
return true; return true;
} }
if (card.isCreature()) { if (card.isCreature()) {
@@ -993,7 +994,7 @@ public class ComputerUtil {
} // BuffedBy } // BuffedBy
// get all cards the human controls with AntiBuffedBy // get all cards the human controls with AntiBuffedBy
final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield); final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
for (Card buffedcard : antibuffed) { for (Card buffedcard : antibuffed) {
if (buffedcard.hasSVar("AntiBuffedBy")) { if (buffedcard.hasSVar("AntiBuffedBy")) {
final String buffedby = buffedcard.getSVar("AntiBuffedBy"); final String buffedby = buffedcard.getSVar("AntiBuffedBy");
@@ -1033,11 +1034,11 @@ public class ComputerUtil {
return ret; return ret;
} else { } else {
// Otherwise, if life is possibly in danger, then this is fine. // Otherwise, if life is possibly in danger, then this is fine.
Combat combat = new Combat(ai.getOpponent()); Combat combat = new Combat(ComputerUtil.getOpponentFor(ai));
CardCollectionView attackers = ai.getOpponent().getCreaturesInPlay(); CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
for (Card att : attackers) { for (Card att : attackers) {
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) { if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
combat.addAttacker(att, att.getController().getOpponent()); combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController()));
} }
} }
AiBlockController aiBlock = new AiBlockController(ai); AiBlockController aiBlock = new AiBlockController(ai);
@@ -1101,7 +1102,7 @@ public class ComputerUtil {
return (sa.getHostCard().isCreature() return (sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost() && sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| !ph.getNextTurn().equals(sa.getActivatingPlayer())) && !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay") && !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases")); && !sa.hasParam("ActivationPhases"));
} }
@@ -1145,7 +1146,7 @@ public class ComputerUtil {
} }
// get all cards the human controls with AntiBuffedBy // get all cards the human controls with AntiBuffedBy
final CardCollectionView antibuffed = ai.getOpponent().getCardsIn(ZoneType.Battlefield); final CardCollectionView antibuffed = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
for (Card buffedcard : antibuffed) { for (Card buffedcard : antibuffed) {
if (buffedcard.hasSVar("AntiBuffedBy")) { if (buffedcard.hasSVar("AntiBuffedBy")) {
final String buffedby = buffedcard.getSVar("AntiBuffedBy"); final String buffedby = buffedcard.getSVar("AntiBuffedBy");
@@ -1331,7 +1332,7 @@ public class ComputerUtil {
if (tgt == null) { if (tgt == null) {
continue; continue;
} }
final Player enemy = ai.getOpponent(); final Player enemy = ComputerUtil.getOpponentFor(ai);
if (!sa.canTarget(enemy)) { if (!sa.canTarget(enemy)) {
continue; continue;
} }
@@ -1436,17 +1437,26 @@ public class ComputerUtil {
objects = canBeTargeted; objects = canBeTargeted;
} }
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) { SpellAbility saviorWithSubs = saviour;
toughness = saviour.hasParam("NumDef") ? ApiType saviorWithSubsApi = saviorWithSubs == null ? null : saviorWithSubs.getApi();
AbilityUtils.calculateAmount(saviour.getHostCard(), saviour.getParam("NumDef"), saviour) : 0; while (saviorWithSubs != null) {
final List<String> keywords = saviour.hasParam("KW") ? ApiType curApi = saviorWithSubs.getApi();
Arrays.asList(saviour.getParam("KW").split(" & ")) : new ArrayList<String>(); if (curApi == ApiType.Pump || curApi == ApiType.PumpAll) {
if (keywords.contains("Indestructible")) { toughness = saviorWithSubs.hasParam("NumDef") ?
grantIndestructible = true; AbilityUtils.calculateAmount(saviorWithSubs.getHostCard(), saviorWithSubs.getParam("NumDef"), saviour) : 0;
} final List<String> keywords = saviorWithSubs.hasParam("KW") ?
if (keywords.contains("Hexproof") || keywords.contains("Shroud")) { Arrays.asList(saviorWithSubs.getParam("KW").split(" & ")) : new ArrayList<String>();
grantShroud = true; if (keywords.contains("Indestructible")) {
grantIndestructible = true;
}
if (keywords.contains("Hexproof") || keywords.contains("Shroud")) {
grantShroud = true;
}
break;
} }
// Consider pump in subabilities, e.g. Bristling Hydra hexproof subability
saviorWithSubs = saviorWithSubs.getSubAbility();
} }
if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) { if (saviourApi == ApiType.PutCounter || saviourApi == ApiType.PutCounterAll) {
@@ -1590,7 +1600,8 @@ public class ComputerUtil {
&& (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll) && (((saviourApi == ApiType.Regenerate || saviourApi == ApiType.RegenerateAll)
&& !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone && !topStack.hasParam("NoRegen")) || saviourApi == ApiType.ChangeZone
|| saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll || saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviourApi == ApiType.Protection || saviourApi == null)) { || saviourApi == ApiType.Protection || saviourApi == null
|| saviorWithSubsApi == ApiType.Pump || saviorWithSubsApi == ApiType.PumpAll)) {
for (final Object o : objects) { for (final Object o : objects) {
if (o instanceof Card) { if (o instanceof Card) {
final Card c = (Card) o; final Card c = (Card) o;
@@ -1604,7 +1615,9 @@ public class ComputerUtil {
continue; continue;
} }
if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll) { if (saviourApi == ApiType.Pump || saviourApi == ApiType.PumpAll
|| saviorWithSubsApi == ApiType.Pump
|| saviorWithSubsApi == ApiType.PumpAll) {
if ((tgt == null && !grantIndestructible) if ((tgt == null && !grantIndestructible)
|| (!grantShroud && !grantIndestructible)) { || (!grantShroud && !grantIndestructible)) {
continue; continue;
@@ -1857,6 +1870,27 @@ public class ComputerUtil {
public static boolean scryWillMoveCardToBottomOfLibrary(Player player, Card c) { public static boolean scryWillMoveCardToBottomOfLibrary(Player player, Card c) {
boolean bottom = false; boolean bottom = false;
// AI profile-based toggles
int maxLandsToScryLandsToTop = 3;
int minLandsToScryLandsAway = 8;
int minCreatsToScryCreatsAway = 5;
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
int lowCMCThreshold = 3;
int maxCreatsToScryLowCMCAway = 3;
boolean uncastablesToBottom = false;
int uncastableCMCThreshold = 1;
if (player.getController().isAI()) {
AiController aic = ((PlayerControllerAi)player.getController()).getAi();
maxLandsToScryLandsToTop = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_STILL_NEED_MORE);
minLandsToScryLandsAway = aic.getIntProperty(AiProps.SCRY_NUM_LANDS_TO_NOT_NEED_MORE);
minCreatsToScryCreatsAway = aic.getIntProperty(AiProps.SCRY_NUM_CREATURES_TO_NOT_NEED_SUBPAR_ONES);
minCreatEvalThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_TO_SCRY_AWAY_LOWCMC_CREATURE);
lowCMCThreshold = aic.getIntProperty(AiProps.SCRY_EVALTHR_CMC_THRESHOLD);
maxCreatsToScryLowCMCAway = aic.getIntProperty(AiProps.SCRY_EVALTHR_CREATCOUNT_TO_SCRY_AWAY_LOWCMC);
uncastablesToBottom = aic.getBooleanProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_TO_BOTTOM);
uncastableCMCThreshold = aic.getIntProperty(AiProps.SCRY_IMMEDIATELY_UNCASTABLE_CMC_DIFF);
}
CardCollectionView allCards = player.getAllCards(); CardCollectionView allCards = player.getAllCards();
CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand); CardCollectionView cardsInHand = player.getCardsIn(ZoneType.Hand);
CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield); CardCollectionView cardsOTB = player.getCardsIn(ZoneType.Battlefield);
@@ -1871,8 +1905,9 @@ public class ComputerUtil {
CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player))); CardCollectionView allCreatures = CardLists.filter(allCards, Predicates.and(CardPredicates.Presets.CREATURES, CardPredicates.isOwner(player)));
int numCards = allCreatures.size(); int numCards = allCreatures.size();
if (landsOTB.size() < 3 && landsInHand.isEmpty()) { if (landsOTB.size() < maxLandsToScryLandsToTop && landsInHand.isEmpty()) {
if ((!c.isLand() && !manaArts.contains(c.getName())) || c.getManaAbilities().isEmpty()) { if ((!c.isLand() && !manaArts.contains(c.getName()))
|| (c.getManaAbilities().isEmpty() && !c.hasABasicLandType())) {
// scry away non-lands and non-manaproducing lands in situations when the land count // scry away non-lands and non-manaproducing lands in situations when the land count
// on the battlefield is low, to try to improve the mana base early // on the battlefield is low, to try to improve the mana base early
bottom = true; bottom = true;
@@ -1880,7 +1915,7 @@ public class ComputerUtil {
} }
if (c.isLand()) { if (c.isLand()) {
if (landsOTB.size() >= 8) { if (landsOTB.size() >= minLandsToScryLandsAway) {
// probably enough lands not to urgently need another one, so look for more gas instead // probably enough lands not to urgently need another one, so look for more gas instead
bottom = true; bottom = true;
} else if (landsInHand.size() >= Math.max(cardsInHand.size() / 2, 2)) { } else if (landsInHand.size() >= Math.max(cardsInHand.size() / 2, 2)) {
@@ -1898,16 +1933,15 @@ public class ComputerUtil {
} else if (c.isCreature()) { } else if (c.isCreature()) {
CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES); CardCollection creaturesOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.CREATURES);
int avgCreatureValue = numCards != 0 ? ComputerUtilCard.evaluateCreatureList(allCreatures) / numCards : 0; int avgCreatureValue = numCards != 0 ? ComputerUtilCard.evaluateCreatureList(allCreatures) / numCards : 0;
int minCreatEvalThreshold = 160; // just a bit higher than a baseline 2/2 creature or a 1/1 mana dork
int maxControlledCMC = Aggregates.max(creaturesOTB, CardPredicates.Accessors.fnGetCmc); int maxControlledCMC = Aggregates.max(creaturesOTB, CardPredicates.Accessors.fnGetCmc);
if (ComputerUtilCard.evaluateCreature(c) < avgCreatureValue) { if (ComputerUtilCard.evaluateCreature(c) < avgCreatureValue) {
if (creaturesOTB.size() > 5) { if (creaturesOTB.size() > minCreatsToScryCreatsAway) {
// if there are more than five creatures and the creature is question is below average for // if there are more than five creatures and the creature is question is below average for
// the deck, scry it to the bottom // the deck, scry it to the bottom
bottom = true; bottom = true;
} else if (creaturesOTB.size() > 3 && c.getCMC() <= 3 } else if (creaturesOTB.size() > maxCreatsToScryLowCMCAway && c.getCMC() <= lowCMCThreshold
&& maxControlledCMC >= 4 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) { && maxControlledCMC >= lowCMCThreshold + 1 && ComputerUtilCard.evaluateCreature(c) <= minCreatEvalThreshold) {
// if we are already at a stage when we have 4+ CMC creatures on the battlefield, // if we are already at a stage when we have 4+ CMC creatures on the battlefield,
// probably worth it to scry away very low value creatures with low CMC // probably worth it to scry away very low value creatures with low CMC
bottom = true; bottom = true;
@@ -1915,6 +1949,15 @@ public class ComputerUtil {
} }
} }
if (uncastablesToBottom && !c.isLand()) {
int cmc = c.isSplitCard() ? Math.min(c.getCMC(Card.SplitCMCMode.LeftSplitCMC), c.getCMC(Card.SplitCMCMode.RightSplitCMC))
: c.getCMC();
int maxCastable = ComputerUtilMana.getAvailableManaEstimate(player, false) + landsInHand.size();
if (cmc - maxCastable >= uncastableCMCThreshold) {
bottom = true;
}
}
return bottom; return bottom;
} }
@@ -2047,7 +2090,7 @@ public class ComputerUtil {
} }
} }
else if (logic.equals("ChosenLandwalk")) { else if (logic.equals("ChosenLandwalk")) {
for (Card c : ai.getOpponent().getLandsInPlay()) { for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
for (String t : c.getType()) { for (String t : c.getType()) {
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) { if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
chosen = t; chosen = t;
@@ -2065,7 +2108,7 @@ public class ComputerUtil {
else if (kindOfType.equals("Land")) { else if (kindOfType.equals("Land")) {
if (logic != null) { if (logic != null) {
if (logic.equals("ChosenLandwalk")) { if (logic.equals("ChosenLandwalk")) {
for (Card c : ai.getOpponent().getLandsInPlay()) { for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
for (String t : c.getType().getLandTypes()) { for (String t : c.getType().getLandTypes()) {
if (!invalidTypes.contains(t)) { if (!invalidTypes.contains(t)) {
chosen = t; chosen = t;
@@ -2098,7 +2141,7 @@ public class ComputerUtil {
case "Torture": case "Torture":
return "Torture"; return "Torture";
case "GraceOrCondemnation": case "GraceOrCondemnation":
return ai.getCreaturesInPlay().size() > ai.getOpponent().getCreaturesInPlay().size() ? "Grace" return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace"
: "Condemnation"; : "Condemnation";
case "CarnageOrHomage": case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists CardCollection cardsInPlay = CardLists
@@ -2683,4 +2726,53 @@ public class ComputerUtil {
} }
return true; return true;
} }
@Deprecated
public static final Player getOpponentFor(final Player player) {
// This method is deprecated and currently functions as a synonym for player.getWeakestOpponent
// until it can be replaced everywhere in the code.
// Consider replacing calls to this method either with a multiplayer-friendly determination of
// opponent that contextually makes the most sense, or with a direct call to player.getWeakestOpponent
// where that is applicable and makes sense from the point of view of multiplayer AI logic.
Player opponent = player.getWeakestOpponent();
if (opponent != null) {
return opponent;
}
throw new IllegalStateException("No opponents left ingame for " + player);
}
public static int countUsefulCreatures(Player p) {
CardCollection creats = p.getCreaturesInPlay();
int count = 0;
for (Card c : creats) {
if (!ComputerUtilCard.isUselessCreature(p, c)) {
count ++;
}
}
return count;
}
public static boolean isPlayingReanimator(final Player ai) {
// TODO: either add SVars to other reanimator cards, or improve the prediction so that it avoids using a SVar
// at all but detects this effect from SA parameters (preferred, but difficult)
CardCollectionView inHand = ai.getCardsIn(ZoneType.Hand);
CardCollectionView inDeck = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Library});
Predicate<Card> markedAsReanimator = new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return "true".equalsIgnoreCase(card.getSVar("IsReanimatorCard"));
}
};
int numInHand = CardLists.filter(inHand, markedAsReanimator).size();
int numInDeck = CardLists.filter(inDeck, markedAsReanimator).size();
return numInHand > 0 || numInDeck >= 3;
}
} }

View File

@@ -1,22 +1,10 @@
package forge.ai; package forge.ai;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.card.CardType; import forge.card.CardType;
import forge.card.ColorSet; import forge.card.ColorSet;
import forge.card.MagicColor; import forge.card.MagicColor;
@@ -29,15 +17,7 @@ import forge.game.GameObject;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayEnergy; import forge.game.cost.CostPayEnergy;
@@ -55,6 +35,12 @@ import forge.game.zone.ZoneType;
import forge.item.PaperCard; import forge.item.PaperCard;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.MyRandom; import forge.util.MyRandom;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.MutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
import java.util.Map.Entry;
public class ComputerUtilCard { public class ComputerUtilCard {
@@ -364,7 +350,12 @@ public class ComputerUtilCard {
} }
if (hasEnchantmants || hasArtifacts) { if (hasEnchantmants || hasArtifacts) {
final List<Card> ae = CardLists.filter(list, Predicates.<Card>or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS)); final List<Card> ae = CardLists.filter(list, Predicates.and(Predicates.<Card>or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.ENCHANTMENTS), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return !card.hasSVar("DoNotDiscardIfAble");
}
}));
return getCheapestPermanentAI(ae, null, false); return getCheapestPermanentAI(ae, null, false);
} }
@@ -377,6 +368,32 @@ public class ComputerUtilCard {
return getCheapestPermanentAI(list, null, false); return getCheapestPermanentAI(list, null, false);
} }
public static final Card getCheapestSpellAI(final Iterable<Card> list) {
if (!Iterables.isEmpty(list)) {
CardCollection cc = CardLists.filter(new CardCollection(list),
Predicates.or(CardPredicates.isType("Instant"), CardPredicates.isType("Sorcery")));
Collections.sort(cc, CardLists.CmcComparatorInv);
if (cc.isEmpty()) {
return null;
}
Card cheapest = cc.getLast();
if (cheapest.hasSVar("DoNotDiscardIfAble")) {
for (int i = cc.size() - 1; i >= 0; i--) {
if (!cc.get(i).hasSVar("DoNotDiscardIfAble")) {
cheapest = cc.get(i);
break;
}
}
}
return cheapest;
}
return null;
}
public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() { public static final Comparator<Card> EvaluateCreatureComparator = new Comparator<Card>() {
@Override @Override
public int compare(final Card a, final Card b) { public int compare(final Card a, final Card b) {
@@ -399,6 +416,10 @@ public class ComputerUtilCard {
return creatureEvaluator.evaluateCreature(c); return creatureEvaluator.evaluateCreature(c);
} }
public static int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
return creatureEvaluator.evaluateCreature(c, considerPT, considerCMC);
}
public static int evaluatePermanentList(final CardCollectionView list) { public static int evaluatePermanentList(final CardCollectionView list) {
int value = 0; int value = 0;
for (int i = 0; i < list.size(); i++) { for (int i = 0; i < list.size(); i++) {
@@ -482,7 +503,7 @@ public class ComputerUtilCard {
*/ */
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) { public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
AiBlockController aiBlk = new AiBlockController(ai); AiBlockController aiBlk = new AiBlockController(ai);
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
Combat combat = new Combat(opp); Combat combat = new Combat(opp);
//Use actual attackers if available, else consider all possible attackers //Use actual attackers if available, else consider all possible attackers
Combat currentCombat = ai.getGame().getCombat(); Combat currentCombat = ai.getGame().getCombat();
@@ -845,9 +866,10 @@ public class ComputerUtilCard {
List<String> chosen = new ArrayList<String>(); List<String> chosen = new ArrayList<String>();
Player ai = sa.getActivatingPlayer(); Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame(); final Game game = ai.getGame();
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic"); final String logic = sa.getParam("AILogic");
if (logic.equals("MostProminentInHumanDeck")) { if (logic.equals("MostProminentInHumanDeck")) {
chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices));
} }
@@ -873,7 +895,7 @@ public class ComputerUtilCard {
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices));
} }
else if (logic.equals("MostProminentHumanControls")) { else if (logic.equals("MostProminentHumanControls")) {
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getOpponent().getCardsIn(ZoneType.Battlefield), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(opp.getCardsIn(ZoneType.Battlefield), colorChoices));
} }
else if (logic.equals("MostProminentPermanent")) { else if (logic.equals("MostProminentPermanent")) {
chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices)); chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices));
@@ -897,7 +919,7 @@ public class ComputerUtilCard {
String bestColor = Constant.GREEN; String bestColor = Constant.GREEN;
for (byte color : MagicColor.WUBRG) { for (byte color : MagicColor.WUBRG) {
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield); CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView opplist = ai.getOpponent().getCardsIn(ZoneType.Battlefield); CardCollectionView opplist = opp.getCardsIn(ZoneType.Battlefield);
ailist = CardLists.filter(ailist, CardPredicates.isColor(color)); ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
opplist = CardLists.filter(opplist, CardPredicates.isColor(color)); opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
@@ -934,7 +956,7 @@ public class ComputerUtilCard {
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) { public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
final Player ai = sa.getActivatingPlayer(); final Player ai = sa.getActivatingPlayer();
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final Game game = ai.getGame(); final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler(); final PhaseHandler ph = game.getPhaseHandler();
final PhaseType phaseType = ph.getPhase(); final PhaseType phaseType = ph.getPhase();
@@ -1174,6 +1196,18 @@ public class ComputerUtilCard {
final PhaseHandler phase = game.getPhaseHandler(); final PhaseHandler phase = game.getPhaseHandler();
final Combat combat = phase.getCombat(); final Combat combat = phase.getCombat();
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic")); final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
final boolean loseCardAtEOT = "Sacrifice".equals(sa.getParam("AtEOT")) || "Exile".equals(sa.getParam("AtEOT"))
|| "Destroy".equals(sa.getParam("AtEOT")) || "ExileCombat".equals(sa.getParam("AtEOT"));
boolean combatTrick = false;
boolean holdCombatTricks = false;
int chanceToHoldCombatTricks = -1;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
holdCombatTricks = aic.getBooleanProperty(AiProps.TRY_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
chanceToHoldCombatTricks = aic.getIntProperty(AiProps.CHANCE_TO_HOLD_COMBAT_TRICKS_UNTIL_BLOCK);
}
if (!c.canBeTargetedBy(sa)) { if (!c.canBeTargetedBy(sa)) {
return false; return false;
@@ -1186,11 +1220,11 @@ public class ComputerUtilCard {
/* -- currently disabled until better conditions are devised and the spell prediction is made smarter -- /* -- currently disabled until better conditions are devised and the spell prediction is made smarter --
// Determine if some mana sources need to be held for the future spell to cast in Main 2 before determining whether to pump. // Determine if some mana sources need to be held for the future spell to cast in Main 2 before determining whether to pump.
AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
if (aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES)) { if (aic.getCardMemory().isMemorySetEmpty(AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
// only hold mana sources once // only hold mana sources once
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Pump); SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Pump);
if (futureSpell != null && futureSpell.getHostCard() != null) { if (futureSpell != null && futureSpell.getHostCard() != null) {
aic.reserveManaSourcesForMain2(futureSpell); aic.reserveManaSources(futureSpell);
} }
} }
*/ */
@@ -1204,8 +1238,8 @@ public class ComputerUtilCard {
return true; return true;
} }
// buff attacker/blocker using triggered pump // buff attacker/blocker using triggered pump (unless it's lethal and we don't want to be reckless)
if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { if (immediately && phase.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && !loseCardAtEOT) {
if (phase.isPlayerTurn(ai)) { if (phase.isPlayerTurn(ai)) {
if (CombatUtil.canAttack(c)) { if (CombatUtil.canAttack(c)) {
return true; return true;
@@ -1217,7 +1251,7 @@ public class ComputerUtilCard {
} }
} }
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords); Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
List<Card> oppCreatures = opp.getCreaturesInPlay(); List<Card> oppCreatures = opp.getCreaturesInPlay();
float chance = 0; float chance = 0;
@@ -1234,6 +1268,28 @@ public class ComputerUtilCard {
threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking threat *= 4; //over-value self +attack for 0 power creatures which may be pumped further after attacking
} }
chance += threat; chance += threat;
// -- Hold combat trick (the AI will try to delay the pump until Declare Blockers) --
// Enable combat trick mode only in case it's a pure buff spell in hand with no keywords or with Trample,
// First Strike, or Double Strike, otherwise the AI is unlikely to cast it or it's too late to
// cast it during Declare Blockers, thus ruining its attacker
if (holdCombatTricks && sa.getApi() == ApiType.Pump
&& sa.hasParam("NumAtt") && sa.getHostCard() != null
&& sa.getHostCard().getZone() != null && sa.getHostCard().getZone().is(ZoneType.Hand)
&& c.getNetPower() > 0 // too obvious if attacking with a 0-power creature
&& sa.getHostCard().isInstant() // only do it for instant speed spells in hand
&& ComputerUtilMana.hasEnoughManaSourcesToCast(sa, ai)) {
combatTrick = true;
final List<String> kws = sa.hasParam("KW") ? Arrays.asList(sa.getParam("KW").split(" & "))
: Lists.<String>newArrayList();
for (String kw : kws) {
if (!kw.equals("Trample") && !kw.equals("First Strike") && !kw.equals("Double Strike")) {
combatTrick = false;
break;
}
}
}
} }
//2. grant haste //2. grant haste
@@ -1261,7 +1317,7 @@ public class ComputerUtilCard {
boolean pumpedWillDie = false; boolean pumpedWillDie = false;
final boolean isAttacking = combat.isAttacking(c); final boolean isAttacking = combat.isAttacking(c);
if (isBerserk && isAttacking) { pumpedWillDie = true; } if ((isBerserk && isAttacking) || loseCardAtEOT) { pumpedWillDie = true; }
if (isAttacking) { if (isAttacking) {
pumpedCombat.addAttacker(pumped, opp); pumpedCombat.addAttacker(pumped, opp);
@@ -1319,6 +1375,16 @@ public class ComputerUtilCard {
if (combat.isAttacking(c) && opp.getLife() > 0) { if (combat.isAttacking(c) && opp.getLife() > 0) {
int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true); int dmg = ComputerUtilCombat.damageIfUnblocked(c, opp, combat, true);
int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true); int pumpedDmg = ComputerUtilCombat.damageIfUnblocked(pumped, opp, pumpedCombat, true);
int poisonOrig = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(c, ai) : 0;
int poisonPumped = opp.canReceiveCounters(CounterType.POISON) ? ComputerUtilCombat.poisonIfUnblocked(pumped, ai) : 0;
// predict Infect
if (pumpedDmg == 0 && c.hasKeyword("Infect")) {
if (poisonPumped > poisonOrig) {
pumpedDmg = poisonPumped;
}
}
if (combat.isBlocked(c)) { if (combat.isBlocked(c)) {
if (!c.hasKeyword("Trample")) { if (!c.hasKeyword("Trample")) {
dmg = 0; dmg = 0;
@@ -1331,8 +1397,11 @@ public class ComputerUtilCard {
pumpedDmg = 0; pumpedDmg = 0;
} }
} }
if (pumpedDmg >= opp.getLife()) { if (pumpedDmg > dmg) {
return true; if ((!c.hasKeyword("Infect") && pumpedDmg >= opp.getLife())
|| (c.hasKeyword("Infect") && opp.canReceiveCounters(CounterType.POISON) && pumpedDmg >= opp.getPoisonCounters())) {
return true;
}
} }
// try to determine if pumping a creature for more power will give lethal on board // try to determine if pumping a creature for more power will give lethal on board
// considering all unblocked creatures after the blockers are already declared // considering all unblocked creatures after the blockers are already declared
@@ -1403,6 +1472,37 @@ public class ComputerUtilCard {
} }
} }
boolean wantToHoldTrick = holdCombatTricks;
if (chanceToHoldCombatTricks >= 0) {
// Obey the chance specified in the AI profile for holding combat tricks
wantToHoldTrick &= MyRandom.percentTrue(chanceToHoldCombatTricks);
} else {
// Use standard considerations dependent solely on the buff chance determined above
wantToHoldTrick &= MyRandom.getRandom().nextFloat() < chance;
}
boolean isHeldCombatTrick = combatTrick && wantToHoldTrick;
if (isHeldCombatTrick) {
if (AiCardMemory.isMemorySetEmpty(ai, AiCardMemory.MemorySet.TRICK_ATTACKERS)) {
// Attempt to hold combat tricks until blockers are declared, and try to lure the opponent into blocking
// (The AI will only do it for one attacker at the moment, otherwise it risks running his attackers into
// an army of opposing blockers with only one combat trick in hand)
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.TRICK_ATTACKERS);
// Reserve the mana until Declare Blockers such that the AI doesn't tap out before having a chance to use
// the combat trick
if (ai.getController().isAI()) {
((PlayerControllerAi) ai.getController()).getAi().reserveManaSources(sa, PhaseType.COMBAT_DECLARE_BLOCKERS);
}
return false;
} else {
// Don't try to mix "lure" and "precast" paradigms for combat tricks, since that creates issues with
// the AI overextending the attack
return false;
}
}
return MyRandom.getRandom().nextFloat() < chance; return MyRandom.getRandom().nextFloat() < chance;
} }
@@ -1417,8 +1517,7 @@ public class ComputerUtilCard {
* @return * @return
*/ */
public static Card getPumpedCreature(final Player ai, final SpellAbility sa, public static Card getPumpedCreature(final Player ai, final SpellAbility sa,
final Card c, final int toughness, final int power, final Card c, int toughness, int power, final List<String> keywords) {
final List<String> keywords) {
Card pumped = CardFactory.copyCard(c, true); Card pumped = CardFactory.copyCard(c, true);
pumped.setSickness(c.hasSickness()); pumped.setSickness(c.hasSickness());
final long timestamp = c.getGame().getNextTimestamp(); final long timestamp = c.getGame().getNextTimestamp();
@@ -1431,9 +1530,19 @@ public class ComputerUtilCard {
} }
} }
// Berserk
final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic")); final boolean isBerserk = "Berserk".equals(sa.getParam("AILogic"));
final int berserkPower = isBerserk ? c.getCurrentPower() : 0; final int berserkPower = isBerserk ? c.getCurrentPower() : 0;
// Electrostatic Pummeler
for (SpellAbility ab : c.getSpellAbilities()) {
if ("Pummeler".equals(ab.getParam("AILogic"))) {
Pair<Integer, Integer> newPT = SpecialCardAi.ElectrostaticPummeler.getPumpedPT(ai, power, toughness);
power = newPT.getLeft();
toughness = newPT.getRight();
}
}
pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp); pumped.addNewPT(c.getCurrentPower(), c.getCurrentToughness(), timestamp);
pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower); pumped.addTempPowerBoost(c.getTempPowerBoost() + power + berserkPower);
pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness); pumped.addTempToughnessBoost(c.getTempToughnessBoost() + toughness);

View File

@@ -17,14 +17,10 @@
*/ */
package forge.ai; package forge.ai;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.game.CardTraitBase; import forge.game.CardTraitBase;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
@@ -32,13 +28,7 @@ import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.card.CounterType;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.CostPayment; import forge.game.cost.CostPayment;
@@ -53,8 +43,12 @@ import forge.game.trigger.Trigger;
import forge.game.trigger.TriggerHandler; import forge.game.trigger.TriggerHandler;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.TextUtil;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
@@ -390,6 +384,18 @@ public class ComputerUtilCombat {
return false; return false;
} }
CardCollectionView otb = ai.getCardsIn(ZoneType.Battlefield);
// Special cases:
// AI can't lose in combat in presence of Worship (with creatures)
if (!CardLists.filter(otb, CardPredicates.nameEquals("Worship")).isEmpty() && !ai.getCreaturesInPlay().isEmpty()) {
return false;
}
// AI can't lose in combat in presence of Elderscale Wurm (at 7 life or more)
if (!CardLists.filter(otb, CardPredicates.nameEquals("Elderscale Wurm")).isEmpty() && ai.getLife() >= 7) {
return false;
}
// check for creatures that must be blocked // check for creatures that must be blocked
final List<Card> attackers = combat.getAttackersOf(ai); final List<Card> attackers = combat.getAttackersOf(ai);
@@ -401,7 +407,11 @@ public class ComputerUtilCombat {
if (blockers.isEmpty()) { if (blockers.isEmpty()) {
if (!attacker.getSVar("MustBeBlocked").equals("")) { if (!attacker.getSVar("MustBeBlocked").equals("")) {
return true; boolean cond = !"attackingplayer".equalsIgnoreCase(attacker.getSVar("MustBeBlocked"))
|| combat.getDefenderByAttacker(attacker) instanceof Player;
if (cond) {
return true;
}
} }
} }
if (threateningCommanders.contains(attacker)) { if (threateningCommanders.contains(attacker)) {
@@ -683,12 +693,21 @@ public class ComputerUtilCombat {
*/ */
public static boolean attackerWouldBeDestroyed(Player ai, final Card attacker, Combat combat) { public static boolean attackerWouldBeDestroyed(Player ai, final Card attacker, Combat combat) {
final List<Card> blockers = combat.getBlockers(attacker); final List<Card> blockers = combat.getBlockers(attacker);
int firstStrikeBlockerDmg = 0;
for (final Card defender : blockers) { for (final Card defender : blockers) {
if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true) if (ComputerUtilCombat.canDestroyAttacker(ai, attacker, defender, combat, true)
&& !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) { && !(defender.hasKeyword("Wither") || defender.hasKeyword("Infect"))) {
return true; return true;
} }
if (defender.hasKeyword("First Strike") || defender.hasKeyword("Double Strike")) {
firstStrikeBlockerDmg += defender.getNetCombatDamage();
}
}
// Consider first strike and double strike
if (attacker.hasKeyword("First Strike") || attacker.hasKeyword("Double Strike")) {
return firstStrikeBlockerDmg >= ComputerUtilCombat.getDamageToKill(attacker);
} }
return ComputerUtilCombat.totalDamageOfBlockers(attacker, blockers) >= ComputerUtilCombat.getDamageToKill(attacker); return ComputerUtilCombat.totalDamageOfBlockers(attacker, blockers) >= ComputerUtilCombat.getDamageToKill(attacker);
@@ -781,7 +800,7 @@ public class ComputerUtilCombat {
if (validBlocked.contains(".withLesserPower")) { if (validBlocked.contains(".withLesserPower")) {
// Have to check this restriction here as triggering objects aren't set yet, so // Have to check this restriction here as triggering objects aren't set yet, so
// ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE // ValidBlocked$Creature.powerLTX where X:TriggeredBlocker$CardPower crashes with NPE
validBlocked = validBlocked.replace(".withLesserPower", ""); validBlocked = TextUtil.fastReplace(validBlocked, ".withLesserPower", "");
if (defender.getCurrentPower() <= attacker.getCurrentPower()) { if (defender.getCurrentPower() <= attacker.getCurrentPower()) {
return false; return false;
} }
@@ -795,7 +814,7 @@ public class ComputerUtilCombat {
if (validBlocker.contains(".withLesserPower")) { if (validBlocker.contains(".withLesserPower")) {
// Have to check this restriction here as triggering objects aren't set yet, so // Have to check this restriction here as triggering objects aren't set yet, so
// ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE // ValidCard$Creature.powerLTX where X:TriggeredAttacker$CardPower crashes with NPE
validBlocker = validBlocker.replace(".withLesserPower", ""); validBlocker = TextUtil.fastReplace(validBlocker, ".withLesserPower", "");
if (defender.getCurrentPower() >= attacker.getCurrentPower()) { if (defender.getCurrentPower() >= attacker.getCurrentPower()) {
return false; return false;
} }
@@ -854,9 +873,12 @@ public class ComputerUtilCombat {
public static int predictPowerBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) { public static int predictPowerBonusOfBlocker(final Card attacker, final Card blocker, boolean withoutAbilities) {
int power = 0; int power = 0;
// Apparently, Flanking is predicted below from a trigger, so using the code below results in double
// application of power bonus. A bit more testing may be needed though, so commenting out for now.
/*
if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) { if (attacker.hasKeyword("Flanking") && !blocker.hasKeyword("Flanking")) {
power -= attacker.getAmountOfKeyword("Flanking"); power -= attacker.getAmountOfKeyword("Flanking");
} }*/
// Serene Master switches power with attacker // Serene Master switches power with attacker
if (blocker.getName().equals("Serene Master")) { if (blocker.getName().equals("Serene Master")) {
@@ -874,7 +896,7 @@ public class ComputerUtilCombat {
power -= attacker.getNetCombatDamage(); power -= attacker.getNetCombatDamage();
} }
power += blocker.getKeywordMagnitude("Bushido"); // power += blocker.getKeywordMagnitude("Bushido"); // This is apparently accounted for in the combat trigs below
final Game game = attacker.getGame(); final Game game = attacker.getGame();
// look out for continuous static abilities that only care for blocking // look out for continuous static abilities that only care for blocking
@@ -889,7 +911,7 @@ public class ComputerUtilCombat {
if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) { if (!params.containsKey("Affected") || !params.get("Affected").contains("blocking")) {
continue; continue;
} }
final String valid = params.get("Affected").replace("blocking", "Creature"); final String valid = TextUtil.fastReplace(params.get("Affected"), "blocking", "Creature");
if (!blocker.isValid(valid, card.getController(), card, null)) { if (!blocker.isValid(valid, card.getController(), card, null)) {
continue; continue;
} }
@@ -1024,7 +1046,7 @@ public class ComputerUtilCombat {
toughness += attacker.getNetToughness() - blocker.getNetToughness(); toughness += attacker.getNetToughness() - blocker.getNetToughness();
} }
toughness += blocker.getKeywordMagnitude("Bushido"); // toughness += blocker.getKeywordMagnitude("Bushido"); // Apparently, this is already accounted for in the combat triggers below
final Game game = attacker.getGame(); final Game game = attacker.getGame();
final FCollection<Trigger> theTriggers = new FCollection<Trigger>(); final FCollection<Trigger> theTriggers = new FCollection<Trigger>();
for (Card card : game.getCardsIn(ZoneType.Battlefield)) { for (Card card : game.getCardsIn(ZoneType.Battlefield)) {
@@ -1176,10 +1198,13 @@ public class ComputerUtilCombat {
* a {@link forge.game.combat.Combat} object. * a {@link forge.game.combat.Combat} object.
* @return a int. * @return a int.
*/ */
public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat , boolean withoutAbilities) { public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat, boolean withoutAbilities) {
return predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, false);
}
public static int predictPowerBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat, boolean withoutAbilities, boolean withoutCombatStaticAbilities) {
int power = 0; int power = 0;
power += attacker.getKeywordMagnitude("Bushido"); // power += attacker.getKeywordMagnitude("Bushido"); // This is apparently accounted for in the combat trigs below
//check Exalted only for the first attacker //check Exalted only for the first attacker
if (combat != null && combat.getAttackers().isEmpty()) { if (combat != null && combat.getAttackers().isEmpty()) {
for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) { for (Card card : attacker.getController().getCardsIn(ZoneType.Battlefield)) {
@@ -1216,27 +1241,29 @@ public class ComputerUtilCombat {
// look out for continuous static abilities that only care for attacking // look out for continuous static abilities that only care for attacking
// creatures // creatures
final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command)); if (!withoutCombatStaticAbilities) {
for (final Card card : cardList) { final CardCollectionView cardList = CardCollection.combine(game.getCardsIn(ZoneType.Battlefield), game.getCardsIn(ZoneType.Command));
for (final StaticAbility stAb : card.getStaticAbilities()) { for (final Card card : cardList) {
final Map<String, String> params = stAb.getMapParams(); for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!params.get("Mode").equals("Continuous")) { final Map<String, String> params = stAb.getMapParams();
continue; if (!params.get("Mode").equals("Continuous")) {
} continue;
if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) { }
continue; if (!params.containsKey("Affected") || !params.get("Affected").contains("attacking")) {
} continue;
final String valid = params.get("Affected").replace("attacking", "Creature"); }
if (!attacker.isValid(valid, card.getController(), card, null)) { final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
continue; if (!attacker.isValid(valid, card.getController(), card, null)) {
} continue;
if (params.containsKey("AddPower")) { }
if (params.get("AddPower").equals("X")) { if (params.containsKey("AddPower")) {
power += CardFactoryUtil.xCount(card, card.getSVar("X")); if (params.get("AddPower").equals("X")) {
} else if (params.get("AddPower").equals("Y")) { power += CardFactoryUtil.xCount(card, card.getSVar("X"));
power += CardFactoryUtil.xCount(card, card.getSVar("Y")); } else if (params.get("AddPower").equals("Y")) {
} else { power += CardFactoryUtil.xCount(card, card.getSVar("Y"));
power += Integer.valueOf(params.get("AddPower")); } else {
power += Integer.valueOf(params.get("AddPower"));
}
} }
} }
} }
@@ -1305,9 +1332,13 @@ public class ComputerUtilCombat {
} else { } else {
String bonus = new String(source.getSVar(att)); String bonus = new String(source.getSVar(att));
if (bonus.contains("TriggerCount$NumBlockers")) { if (bonus.contains("TriggerCount$NumBlockers")) {
bonus = bonus.replace("TriggerCount$NumBlockers", "Number$1"); bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
bonus = bonus.replace("TriggeredPlayersDefenders$Amount", "Number$1"); bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
} else if (bonus.contains("TriggeredAttacker$CardPower")) { // e.g. Arahbo, Roar of the World
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardPower", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetPower())));
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
bonus = TextUtil.fastReplace(bonus, "TriggeredAttacker$CardToughness", TextUtil.concatNoSpace("Number$", String.valueOf(attacker.getNetToughness())));
} }
power += CardFactoryUtil.xCount(source, bonus); power += CardFactoryUtil.xCount(source, bonus);
@@ -1372,6 +1403,10 @@ public class ComputerUtilCombat {
*/ */
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
, boolean withoutAbilities) { , boolean withoutAbilities) {
return predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, false);
}
public static int predictToughnessBonusOfAttacker(final Card attacker, final Card blocker, final Combat combat
, boolean withoutAbilities, boolean withoutCombatStaticAbilities) {
int toughness = 0; int toughness = 0;
//check Exalted only for the first attacker //check Exalted only for the first attacker
@@ -1394,43 +1429,45 @@ public class ComputerUtilCombat {
theTriggers.addAll(card.getTriggers()); theTriggers.addAll(card.getTriggers());
} }
if (blocker != null) { if (blocker != null) {
toughness += attacker.getKeywordMagnitude("Bushido"); // toughness += attacker.getKeywordMagnitude("Bushido"); // This is apparently accounted for in the combat trigs below
theTriggers.addAll(blocker.getTriggers()); theTriggers.addAll(blocker.getTriggers());
} }
// look out for continuous static abilities that only care for attacking // look out for continuous static abilities that only care for attacking
// creatures // creatures
final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield); if (!withoutCombatStaticAbilities) {
for (final Card card : cardList) { final CardCollectionView cardList = game.getCardsIn(ZoneType.Battlefield);
for (final StaticAbility stAb : card.getStaticAbilities()) { for (final Card card : cardList) {
final Map<String, String> params = stAb.getMapParams(); for (final StaticAbility stAb : card.getStaticAbilities()) {
if (!params.get("Mode").equals("Continuous")) { final Map<String, String> params = stAb.getMapParams();
continue; if (!params.get("Mode").equals("Continuous")) {
} continue;
if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) { }
final String valid = params.get("Affected").replace("attacking", "Creature"); if (params.containsKey("Affected") && params.get("Affected").contains("attacking")) {
if (!attacker.isValid(valid, card.getController(), card, null)) { final String valid = TextUtil.fastReplace(params.get("Affected"), "attacking", "Creature");
continue; if (!attacker.isValid(valid, card.getController(), card, null)) {
} continue;
if (params.containsKey("AddToughness")) { }
if (params.get("AddToughness").equals("X")) { if (params.containsKey("AddToughness")) {
toughness += CardFactoryUtil.xCount(card, card.getSVar("X")); if (params.get("AddToughness").equals("X")) {
} else if (params.get("AddToughness").equals("Y")) { toughness += CardFactoryUtil.xCount(card, card.getSVar("X"));
toughness += CardFactoryUtil.xCount(card, card.getSVar("Y")); } else if (params.get("AddToughness").equals("Y")) {
} else { toughness += CardFactoryUtil.xCount(card, card.getSVar("Y"));
toughness += Integer.valueOf(params.get("AddToughness")); } else {
} toughness += Integer.valueOf(params.get("AddToughness"));
} }
} else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) { }
final String valid = params.get("Affected").replace("untapped", "Creature"); } else if (params.containsKey("Affected") && params.get("Affected").contains("untapped")) {
if (!attacker.isValid(valid, card.getController(), card, null) final String valid = TextUtil.fastReplace(params.get("Affected"), "untapped", "Creature");
|| attacker.hasKeyword("Vigilance")) { if (!attacker.isValid(valid, card.getController(), card, null)
continue; || attacker.hasKeyword("Vigilance")) {
} continue;
// remove the bonus, because it will no longer be granted }
if (params.containsKey("AddToughness")) { // remove the bonus, because it will no longer be granted
toughness -= Integer.valueOf(params.get("AddToughness")); if (params.containsKey("AddToughness")) {
} toughness -= Integer.valueOf(params.get("AddToughness"));
}
}
} }
} }
} }
@@ -1517,9 +1554,9 @@ public class ComputerUtilCombat {
} else { } else {
String bonus = new String(source.getSVar(def)); String bonus = new String(source.getSVar(def));
if (bonus.contains("TriggerCount$NumBlockers")) { if (bonus.contains("TriggerCount$NumBlockers")) {
bonus = bonus.replace("TriggerCount$NumBlockers", "Number$1"); bonus = TextUtil.fastReplace(bonus, "TriggerCount$NumBlockers", "Number$1");
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee } else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
bonus = bonus.replace("TriggeredPlayersDefenders$Amount", "Number$1"); bonus = TextUtil.fastReplace(bonus, "TriggeredPlayersDefenders$Amount", "Number$1");
} }
toughness += CardFactoryUtil.xCount(source, bonus); toughness += CardFactoryUtil.xCount(source, bonus);
} }
@@ -1674,6 +1711,10 @@ public class ComputerUtilCombat {
*/ */
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat, public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
final boolean withoutAbilities) { final boolean withoutAbilities) {
return canDestroyAttacker(ai, attacker, blocker, combat, withoutAbilities, false);
}
public static boolean canDestroyAttacker(Player ai, Card attacker, Card blocker, final Combat combat,
final boolean withoutAbilities, final boolean withoutAttackerStaticAbilities) {
// Can activate transform ability // Can activate transform ability
if (!withoutAbilities) { if (!withoutAbilities) {
attacker = canTransform(attacker); attacker = canTransform(attacker);
@@ -1725,10 +1766,10 @@ public class ComputerUtilCombat {
} }
if (attacker.toughnessAssignsDamage()) { if (attacker.toughnessAssignsDamage()) {
attackerDamage = attacker.getNetToughness() attackerDamage = attacker.getNetToughness()
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
} else { } else {
attackerDamage = attacker.getNetPower() attackerDamage = attacker.getNetPower()
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities); + ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
} }
int possibleDefenderPrevention = 0; int possibleDefenderPrevention = 0;
@@ -1741,11 +1782,16 @@ public class ComputerUtilCombat {
// consider Damage Prevention/Replacement // consider Damage Prevention/Replacement
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true); defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, true);
attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true); attackerDamage = predictDamageTo(blocker, attackerDamage, possibleDefenderPrevention, attacker, true);
if (!attacker.getGame().getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noPrevention)) {
if (defenderDamage > 0 && isCombatDamagePrevented(blocker, attacker, defenderDamage)) {
return false;
}
}
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker) final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities); + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker) final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
if (blocker.hasKeyword("Double Strike")) { if (blocker.hasKeyword("Double Strike")) {
if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) { if (defenderDamage > 0 && (hasKeyword(blocker, "Deathtouch", withoutAbilities, combat) || attacker.hasSVar("DestroyWhenDamaged"))) {
@@ -1914,6 +1960,10 @@ public class ComputerUtilCombat {
*/ */
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat, public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
final boolean withoutAbilities) { final boolean withoutAbilities) {
return canDestroyBlocker(ai, blocker, attacker, combat, withoutAbilities, false);
}
public static boolean canDestroyBlocker(Player ai, Card blocker, Card attacker, final Combat combat,
final boolean withoutAbilities, final boolean withoutAttackerStaticAbilities) {
// Can activate transform ability // Can activate transform ability
if (!withoutAbilities) { if (!withoutAbilities) {
attacker = canTransform(attacker); attacker = canTransform(attacker);
@@ -1947,10 +1997,10 @@ public class ComputerUtilCombat {
} }
if (attacker.toughnessAssignsDamage()) { if (attacker.toughnessAssignsDamage()) {
attackerDamage = attacker.getNetToughness() attackerDamage = attacker.getNetToughness()
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
} else { } else {
attackerDamage = attacker.getNetPower() attackerDamage = attacker.getNetPower()
+ ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities); + ComputerUtilCombat.predictPowerBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
} }
int possibleDefenderPrevention = 0; int possibleDefenderPrevention = 0;
@@ -1975,7 +2025,7 @@ public class ComputerUtilCombat {
final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker) final int defenderLife = ComputerUtilCombat.getDamageToKill(blocker)
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities); + ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker) final int attackerLife = ComputerUtilCombat.getDamageToKill(attacker)
+ ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities); + ComputerUtilCombat.predictToughnessBonusOfAttacker(attacker, blocker, combat, withoutAbilities, withoutAttackerStaticAbilities);
if (attacker.hasKeyword("Double Strike")) { if (attacker.hasKeyword("Double Strike")) {
if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) { if (attackerDamage > 0 && (hasKeyword(attacker, "Deathtouch", withoutAbilities, combat) || blocker.hasSVar("DestroyWhenDamaged"))) {
@@ -2192,6 +2242,7 @@ public class ComputerUtilCombat {
*/ */
public final static int getDamageToKill(final Card c) { public final static int getDamageToKill(final Card c) {
int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields(); int killDamage = c.getLethalDamage() + c.getPreventNextDamageTotalShields();
if ((killDamage > c.getPreventNextDamageTotalShields()) if ((killDamage > c.getPreventNextDamageTotalShields())
&& c.hasSVar("DestroyWhenDamaged")) { && c.hasSVar("DestroyWhenDamaged")) {
killDamage = 1 + c.getPreventNextDamageTotalShields(); killDamage = 1 + c.getPreventNextDamageTotalShields();
@@ -2431,6 +2482,94 @@ public class ComputerUtilCombat {
int afflictDmg = attacker.getKeywordMagnitude("Afflict"); int afflictDmg = attacker.getKeywordMagnitude("Afflict");
return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife(); return afflictDmg > attacker.getNetPower() || afflictDmg >= aiDefender.getLife();
} }
public static int getMaxAttackersFor(final GameEntity defender) {
if (defender instanceof Player) {
for (final Card card : ((Player) defender).getCardsIn(ZoneType.Battlefield)) {
if (card.hasKeyword("No more than one creature can attack you each combat.")) {
return 1;
} else if (card.hasKeyword("No more than two creatures can attack you each combat.")) {
return 2;
}
}
}
return -1;
}
public static List<Card> categorizeAttackersByEvasion(List<Card> attackers) {
List<Card> categorizedAttackers = Lists.newArrayList();
CardCollection withEvasion = new CardCollection();
CardCollection withoutEvasion = new CardCollection();
for (Card atk : attackers) {
boolean hasProtection = false;
for (String kw : atk.getKeywords()) {
if (kw.startsWith("Protection")) {
hasProtection = true;
break;
}
}
if (atk.hasKeyword("Flying") || atk.hasKeyword("Shadow")
|| atk.hasKeyword("Horsemanship") || (atk.hasKeyword("Fear")
|| atk.hasKeyword("Intimidate") || atk.hasKeyword("Skulk") || hasProtection)) {
withEvasion.add(atk);
} else {
withoutEvasion.add(atk);
}
}
// attackers that can only be blocked by cards with specific keywords or color, etc.
// (maybe will need to split into 2 or 3 tiers depending on importance)
categorizedAttackers.addAll(withEvasion);
// all other attackers that have no evasion
// (Menace and other abilities that limit blocking by amount of blockers is likely handled
// elsewhere, but that needs testing and possibly fine-tuning).
categorizedAttackers.addAll(withoutEvasion);
return categorizedAttackers;
}
public static Card applyPotentialAttackCloneTriggers(Card attacker) {
// This method returns the potentially cloned card if the creature turns into something else during the attack
// (currently looks for the creature with maximum raw power since that's what the AI usually judges by when
// deciding whether the creature is worth blocking).
// If the creature doesn't change into anything, returns the original creature.
if (attacker == null) { return null; }
Card attackerAfterTrigs = attacker;
// Test for some special triggers that can change the creature in combat
for (Trigger t : attacker.getTriggers()) {
if (t.getMode() == TriggerType.Attacks && t.hasParam("Execute")) {
if (!attacker.hasSVar(t.getParam("Execute"))) {
continue;
}
SpellAbility exec = AbilityFactory.getAbility(attacker, t.getParam("Execute"));
if (exec != null) {
if (exec.getApi() == ApiType.Clone && "Self".equals(exec.getParam("CloneTarget"))
&& exec.hasParam("ValidTgts") && exec.getParam("ValidTgts").contains("Creature")
&& exec.getParam("ValidTgts").contains("attacking")) {
// Tilonalli's Skinshifter and potentially other similar cards that can clone other stuff
// while attacking
if (exec.getParam("ValidTgts").contains("nonLegendary") && attacker.getType().isLegendary()) {
continue;
}
int maxPwr = 0;
for (Card c : attacker.getController().getCreaturesInPlay()) {
if (c.getNetPower() > maxPwr || (c.getNetPower() == maxPwr && ComputerUtilCard.evaluateCreature(c) > ComputerUtilCard.evaluateCreature(attackerAfterTrigs))) {
maxPwr = c.getNetPower();
attackerAfterTrigs = c;
}
}
}
}
}
}
return attackerAfterTrigs;
}
} }

View File

@@ -90,6 +90,15 @@ public class ComputerUtilCost {
return false; return false;
} }
// Remove X counters - set ChosenX to max possible value here, the SAs should correct that
// value later as the AI decides what to do (in checkApiLogic / checkAiLogic)
if (sa != null && sa.hasSVar(remCounter.getAmount())) {
final String sVar = sa.getSVar(remCounter.getAmount());
if (sVar.equals("XChoice")) {
sa.setSVar("ChosenX", String.valueOf(source.getCounters(type)));
}
}
// check the sa what the PaymentDecision is. // check the sa what the PaymentDecision is.
// ignore Loyality abilities with Zero as Cost // ignore Loyality abilities with Zero as Cost
if (sa != null && !CounterType.LOYALTY.equals(type)) { if (sa != null && !CounterType.LOYALTY.equals(type)) {
@@ -231,13 +240,15 @@ public class ComputerUtilCost {
* the source * the source
* @return true, if successful * @return true, if successful
*/ */
public static boolean checkCreatureSacrificeCost(final Player ai, final Cost cost, final Card source) { public static boolean checkCreatureSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
if (cost == null) { if (cost == null) {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) { if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part; final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
if (sac.payCostFromSource() && source.isCreature()) { if (sac.payCostFromSource() && source.isCreature()) {
return false; return false;
} }
@@ -247,9 +258,18 @@ public class ComputerUtilCost {
continue; continue;
} }
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(","), source.getController(), source, null); final CardCollection sacList = new CardCollection();
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) { final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
return false;
int count = 0;
while (count < amount) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
if (prefCard == null) {
return false;
}
sacList.add(prefCard);
typeList.remove(prefCard);
count++;
} }
} }
} }
@@ -267,13 +287,14 @@ public class ComputerUtilCost {
* is the gain important enough? * is the gain important enough?
* @return true, if successful * @return true, if successful
*/ */
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final boolean important) { public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility, final boolean important) {
if (cost == null) { if (cost == null) {
return true; return true;
} }
for (final CostPart part : cost.getCostParts()) { for (final CostPart part : cost.getCostParts()) {
if (part instanceof CostSacrifice) { if (part instanceof CostSacrifice) {
final CostSacrifice sac = (CostSacrifice) part; final CostSacrifice sac = (CostSacrifice) part;
final int amount = AbilityUtils.calculateAmount(source, sac.getAmount(), sourceAbility);
final String type = sac.getType(); final String type = sac.getType();
@@ -286,9 +307,19 @@ public class ComputerUtilCost {
} }
continue; continue;
} }
final CardCollection sacList = new CardCollection();
final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null); final CardCollection typeList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), type.split(";"), source.getController(), source, null);
if (ComputerUtil.getCardPreference(ai, source, "SacCost", typeList) == null) {
return false; int count = 0;
while (count < amount) {
Card prefCard = ComputerUtil.getCardPreference(ai, source, "SacCost", typeList);
if (prefCard == null) {
return false;
}
sacList.add(prefCard);
typeList.remove(prefCard);
count++;
} }
} }
} }
@@ -336,7 +367,7 @@ public class ComputerUtilCost {
final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle); final int vehicleValue = ComputerUtilCard.evaluateCreature(vehicle);
String type = part.getType(); String type = part.getType();
String totalP = type.split("withTotalPowerGE")[1]; String totalP = type.split("withTotalPowerGE")[1];
type = type.replace("+withTotalPowerGE" + totalP, ""); type = TextUtil.fastReplace(type, TextUtil.concatNoSpace("+withTotalPowerGE", totalP), "");
CardCollection exclude = CardLists.getValidCards( CardCollection exclude = CardLists.getValidCards(
new CardCollection(ai.getCardsIn(ZoneType.Battlefield)), type.split(";"), new CardCollection(ai.getCardsIn(ZoneType.Battlefield)), type.split(";"),
source.getController(), source, sa); source.getController(), source, sa);
@@ -364,8 +395,8 @@ public class ComputerUtilCost {
* the source * the source
* @return true, if successful * @return true, if successful
*/ */
public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source) { public static boolean checkSacrificeCost(final Player ai, final Cost cost, final Card source, final SpellAbility sourceAbility) {
return checkSacrificeCost(ai, cost, source, true); return checkSacrificeCost(ai, cost, source, sourceAbility,true);
} }
/** /**
@@ -561,12 +592,12 @@ public class ComputerUtilCost {
return checkLifeCost(payer, cost, source, 4, sa) return checkLifeCost(payer, cost, source, 4, sa)
&& checkDamageCost(payer, cost, source, 4) && checkDamageCost(payer, cost, source, 4)
&& (isMine || checkSacrificeCost(payer, cost, source)) && (isMine || checkSacrificeCost(payer, cost, source, sa))
&& (isMine || checkDiscardCost(payer, cost, source)) && (isMine || checkDiscardCost(payer, cost, source))
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2) && (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2) && (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1) && (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (payer.getOpponent().getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3)); && (!source.getName().equals("Chain of Vapor") || (ComputerUtil.getOpponentFor(payer).getCreaturesInPlay().size() > 0 && payer.getLandsInPlay().size() > 3));
} }
public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) { public static Set<String> getAvailableManaColors(Player ai, Card additionalLand) {

View File

@@ -1,12 +1,8 @@
package forge.ai; package forge.ai;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap; import com.google.common.base.Predicates;
import com.google.common.collect.ListMultimap; import com.google.common.collect.*;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.ai.ability.AnimateAi; import forge.ai.ability.AnimateAi;
import forge.card.ColorSet; import forge.card.ColorSet;
import forge.card.MagicColor; import forge.card.MagicColor;
@@ -18,11 +14,7 @@ import forge.game.Game;
import forge.game.GameActionUtil; import forge.game.GameActionUtil;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardUtil;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostAdjustment; import forge.game.cost.CostAdjustment;
@@ -41,7 +33,6 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil; import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
@@ -203,41 +194,72 @@ public class ComputerUtilMana {
continue; continue;
} }
if (sa.getHostCard() != null && sa.getApi() == ApiType.Animate) { if (sa.getHostCard() != null) {
// For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana if (sa.getApi() == ApiType.Animate) {
if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined")) // For abilities like Genju of the Cedars, make sure that we're not activating the aura ability by tapping the enchanted card for mana
&& ma.getHostCard() == sa.getHostCard().getEnchantingCard() if (sa.getHostCard().isAura() && "Enchanted".equals(sa.getParam("Defined"))
&& ma.getPayCosts().hasTapCost()) { && ma.getHostCard() == sa.getHostCard().getEnchantingCard()
continue; && ma.getPayCosts().hasTapCost()) {
continue;
}
// If a manland was previously animated this turn, do not tap it to animate another manland
if (sa.getHostCard().isLand() && ma.getHostCard().isLand()
&& ai.getController().isAI()
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
continue;
}
} else if (sa.getApi() == ApiType.Pump) {
if ((sa.getHostCard().isInstant() || sa.getHostCard().isSorcery())
&& ma.getHostCard().isCreature()
&& ai.getController().isAI()
&& ma.getPayCosts().hasTapCost()
&& sa.getTargets().getTargetCards().contains(ma.getHostCard())) {
// do not activate pump instants/sorceries targeting creatures by tapping targeted
// creatures for mana (for example, Servant of the Conduit)
continue;
}
} else if (sa.getApi() == ApiType.Attach
&& "AvoidPayingWithAttachTarget".equals(sa.getHostCard().getSVar("AIPaymentPreference"))) {
// For cards like Genju of the Cedars, make sure we're not attaching to the same land that will
// be tapped to pay its own cost if there's another untapped land like that available
if (ma.getHostCard().equals(sa.getTargetCard())) {
if (CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.nameEquals(ma.getHostCard().getName()), CardPredicates.Presets.UNTAPPED)).size() > 1) {
continue;
}
}
} }
// If a manland was previously animated this turn, do not tap it to animate another manland
if (sa.getHostCard().isLand() && ma.getHostCard().isLand()
&& ai.getController() instanceof PlayerControllerAi
&& AnimateAi.isAnimatedThisTurn(ai, ma.getHostCard())) {
continue;
}
} }
SpellAbility paymentChoice = ma;
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability // Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
// to attempt to make the spell uncounterable when possible. // to attempt to make the spell uncounterable when possible.
if ((toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
&& ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) { && sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
for (SpellAbility ab : saList) { if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) { // Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
return ab; continue;
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
for (SpellAbility ab : saList) {
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
if (!ab.getHostCard().isTapped()) {
paymentChoice = ab;
break;
}
}
} }
} }
} }
final String typeRes = cost.getSourceRestriction(); final String typeRes = cost.getSourceRestriction();
if (StringUtils.isNotBlank(typeRes) && !ma.getHostCard().getType().hasStringType(typeRes)) { if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().getType().hasStringType(typeRes)) {
continue; continue;
} }
if (canPayShardWithSpellAbility(toPay, ai, ma, sa, checkCosts)) { if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts)) {
return ma; return paymentChoice;
} }
} }
return null; return null;
@@ -802,7 +824,7 @@ public class ComputerUtilMana {
} }
// isManaSourceReserved returns true if sourceCard is reserved as a mana source for payment // isManaSourceReserved returns true if sourceCard is reserved as a mana source for payment
// for the future spell to be cast in Mana 2. However, if "sa" (the spell ability that is // for the future spell to be cast in another phase. However, if "sa" (the spell ability that is
// being considered for casting) is high priority, then mana source reservation will be // being considered for casting) is high priority, then mana source reservation will be
// ignored. // ignored.
private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbility sa) { private static boolean isManaSourceReserved(Player ai, Card sourceCard, SpellAbility sa) {
@@ -816,8 +838,21 @@ public class ComputerUtilMana {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi(); AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE); int chanceToReserve = aic.getIntProperty(AiProps.RESERVE_MANA_FOR_MAIN2_CHANCE);
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
// For combat tricks, always obey mana reservation
if (curPhase == PhaseType.COMBAT_DECLARE_BLOCKERS || curPhase == PhaseType.CLEANUP) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK);
}
else {
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_DECLBLK)) {
// This mana source is held elsewhere for a combat trick.
return true;
}
}
// If it's a low priority spell (it's explicitly marked so elsewhere in the AI with a SVar), always // If it's a low priority spell (it's explicitly marked so elsewhere in the AI with a SVar), always
// obey mana reservations; otherwise, obey mana reservations depending on the "chance to reserve" // obey mana reservations for Main 2; otherwise, obey mana reservations depending on the "chance to reserve"
// AI profile variable. // AI profile variable.
if (sa.getSVar("LowPriorityAI").equals("")) { if (sa.getSVar("LowPriorityAI").equals("")) {
if (chanceToReserve == 0 || MyRandom.getRandom().nextInt(100) >= chanceToReserve) { if (chanceToReserve == 0 || MyRandom.getRandom().nextInt(100) >= chanceToReserve) {
@@ -825,16 +860,16 @@ public class ComputerUtilMana {
} }
} }
PhaseType curPhase = ai.getGame().getPhaseHandler().getPhase();
if (curPhase == PhaseType.MAIN2 || curPhase == PhaseType.CLEANUP) { if (curPhase == PhaseType.MAIN2 || curPhase == PhaseType.CLEANUP) {
AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES); AiCardMemory.clearMemorySet(ai, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2);
} }
else { else {
if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) { if (AiCardMemory.isRememberedCard(ai, sourceCard, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
// This mana source is held elsewhere for a Main Phase 2 spell. // This mana source is held elsewhere for a Main Phase 2 spell.
return true; return true;
} }
} }
return false; return false;
} }
@@ -1092,8 +1127,65 @@ public class ComputerUtilMana {
return cost; return cost;
} }
// This method can be used to estimate the total amount of mana available to the player,
// including the mana available in that player's mana pool
public static int getAvailableManaEstimate(final Player p) {
return getAvailableManaEstimate(p, true);
}
public static int getAvailableManaEstimate(final Player p, final boolean checkPlayable) {
int availableMana = 0;
final CardCollectionView list = new CardCollection(p.getCardsIn(ZoneType.Battlefield));
final List<Card> srcs = CardLists.filter(list, new Predicate<Card>() {
@Override
public boolean apply(final Card c) {
return !c.getManaAbilities().isEmpty();
}
});
int maxProduced = 0;
int producedWithCost = 0;
boolean hasSourcesWithNoManaCost = false;
for (Card src : srcs) {
maxProduced = 0;
for (SpellAbility ma : src.getManaAbilities()) {
ma.setActivatingPlayer(p);
if (!checkPlayable || ma.canPlay()) {
int costsToActivate = ma.getPayCosts() != null && ma.getPayCosts().getCostMana() != null ? ma.getPayCosts().getCostMana().convertAmount() : 0;
int producedMana = ma.getParamOrDefault("Produced", "").split(" ").length;
int producedAmount = AbilityUtils.calculateAmount(src, ma.getParamOrDefault("Amount", "1"), ma);
int producedTotal = producedMana * producedAmount - costsToActivate;
if (costsToActivate > 0) {
producedWithCost += producedTotal;
} else if (!hasSourcesWithNoManaCost) {
hasSourcesWithNoManaCost = true;
}
if (producedTotal > maxProduced) {
maxProduced = producedTotal;
}
}
}
availableMana += maxProduced;
}
availableMana += p.getManaPool().totalMana();
if (producedWithCost > 0 && !hasSourcesWithNoManaCost) {
availableMana -= producedWithCost; // probably can't activate them, no other mana available
}
return availableMana;
}
//This method is currently used by AI to estimate available mana //This method is currently used by AI to estimate available mana
public static CardCollection getAvailableMana(final Player ai, final boolean checkPlayable) { public static CardCollection getAvailableManaSources(final Player ai, final boolean checkPlayable) {
final CardCollectionView list = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand)); final CardCollectionView list = CardCollection.combine(ai.getCardsIn(ZoneType.Battlefield), ai.getCardsIn(ZoneType.Hand));
final List<Card> manaSources = CardLists.filter(list, new Predicate<Card>() { final List<Card> manaSources = CardLists.filter(list, new Predicate<Card>() {
@Override @Override
@@ -1200,7 +1292,7 @@ public class ComputerUtilMana {
System.out.println("DEBUG_MANA_PAYMENT: sortedManaSources = " + sortedManaSources); System.out.println("DEBUG_MANA_PAYMENT: sortedManaSources = " + sortedManaSources);
} }
return sortedManaSources; return sortedManaSources;
} // getAvailableMana() } // getAvailableManaSources()
//This method is currently used by AI to estimate mana available //This method is currently used by AI to estimate mana available
private static ListMultimap<Integer, SpellAbility> groupSourcesByManaColor(final Player ai, boolean checkPlayable) { private static ListMultimap<Integer, SpellAbility> groupSourcesByManaColor(final Player ai, boolean checkPlayable) {
@@ -1221,7 +1313,7 @@ public class ComputerUtilMana {
} }
// Loop over all current available mana sources // Loop over all current available mana sources
for (final Card sourceCard : getAvailableMana(ai, checkPlayable)) { for (final Card sourceCard : getAvailableManaSources(ai, checkPlayable)) {
if (DEBUG_MANA_PAYMENT) { if (DEBUG_MANA_PAYMENT) {
System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor sourceCard = " + sourceCard); System.out.println("DEBUG_MANA_PAYMENT: groupSourcesByManaColor sourceCard = " + sourceCard);
} }
@@ -1264,7 +1356,7 @@ public class ComputerUtilMana {
Card crd = replacementEffect.getHostCard(); Card crd = replacementEffect.getHostCard();
String repType = crd.getSVar(replacementEffect.getMapParams().get("ManaReplacement")); String repType = crd.getSVar(replacementEffect.getMapParams().get("ManaReplacement"));
if (repType.contains("Chosen")) { if (repType.contains("Chosen")) {
repType = repType.replace("Chosen", MagicColor.toShortString(crd.getChosenColor())); repType = TextUtil.fastReplace(repType, "Chosen", MagicColor.toShortString(crd.getChosenColor()));
} }
mp.setManaReplaceType(repType); mp.setManaReplaceType(repType);
} }

View File

@@ -1,8 +1,10 @@
package forge.ai; package forge.ai;
import com.google.common.base.Function; import com.google.common.base.Function;
import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterType;
import forge.game.cost.CostPayEnergy;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
public class CreatureEvaluator implements Function<Card, Integer> { public class CreatureEvaluator implements Function<Card, Integer> {
@@ -19,6 +21,10 @@ public class CreatureEvaluator implements Function<Card, Integer> {
} }
public int evaluateCreature(final Card c) { public int evaluateCreature(final Card c) {
return evaluateCreature(c, true, true);
}
public int evaluateCreature(final Card c, final boolean considerPT, final boolean considerCMC) {
int value = 80; int value = 80;
if (!c.isToken()) { if (!c.isToken()) {
value += addValue(20, "non-token"); // tokens should be worth less than actual cards value += addValue(20, "non-token"); // tokens should be worth less than actual cards
@@ -34,9 +40,13 @@ public class CreatureEvaluator implements Function<Card, Integer> {
break; break;
} }
} }
value += addValue(power * 15, "power"); if (considerPT) {
value += addValue(toughness * 10, "toughness: " + toughness); value += addValue(power * 15, "power");
value += addValue(c.getCMC() * 5, "cmc"); value += addValue(toughness * 10, "toughness: " + toughness);
}
if (considerCMC) {
value += addValue(c.getCMC() * 5, "cmc");
}
// Evasion keywords // Evasion keywords
if (c.hasKeyword("Flying")) { if (c.hasKeyword("Flying")) {
@@ -91,6 +101,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(power * 15, "infect"); value += addValue(power * 15, "infect");
} }
value += addValue(c.getKeywordMagnitude("Rampage"), "rampage"); value += addValue(c.getKeywordMagnitude("Rampage"), "rampage");
value += addValue(c.getKeywordMagnitude("Afflict") * 5, "afflict");
} }
value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido"); value += addValue(c.getKeywordMagnitude("Bushido") * 16, "bushido");
@@ -99,6 +110,14 @@ public class CreatureEvaluator implements Function<Card, Integer> {
value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi"); value += addValue(c.getKeywordMagnitude("Annihilator") * 50, "eldrazi");
value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb"); value += addValue(c.getKeywordMagnitude("Absorb") * 11, "absorb");
// Keywords that may produce temporary or permanent buffs over time
if (c.hasKeyword("Prowess")) {
value += addValue(5, "prowess");
}
if (c.hasKeyword("Outlast")) {
value += addValue(10, "outlast");
}
// Defensive Keywords // Defensive Keywords
if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) { if (c.hasKeyword("Reach") && !c.hasKeyword("Flying")) {
value += addValue(5, "reach"); value += addValue(5, "reach");
@@ -184,7 +203,7 @@ public class CreatureEvaluator implements Function<Card, Integer> {
for (final SpellAbility sa : c.getSpellAbilities()) { for (final SpellAbility sa : c.getSpellAbilities()) {
if (sa.isAbility()) { if (sa.isAbility()) {
value += addValue(10, "sa: " + sa); value += addValue(evaluateSpellAbility(sa), "sa: " + sa);
} }
} }
if (!c.getManaAbilities().isEmpty()) { if (!c.getManaAbilities().isEmpty()) {
@@ -203,9 +222,38 @@ public class CreatureEvaluator implements Function<Card, Integer> {
if (!c.getEncodedCards().isEmpty()) { if (!c.getEncodedCards().isEmpty()) {
value += addValue(24, "encoded"); value += addValue(24, "encoded");
} }
return value; return value;
} }
private int evaluateSpellAbility(SpellAbility sa) {
// Pump abilities
if (sa.getApi() == ApiType.Pump) {
// Pump abilities that grant +X/+X to the card
if ("+X".equals(sa.getParam("NumAtt"))
&& "+X".equals(sa.getParam("NumDef"))
&& !sa.usesTargeting()
&& (!sa.hasParam("Defined") || "Self".equals(sa.getParam("Defined")))) {
if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostPayEnergy.class)) {
// Electrostatic Pummeler, can be expanded for similar cards
int initPower = getEffectivePower(sa.getHostCard());
int pumpedPower = initPower;
int energy = sa.getHostCard().getController().getCounters(CounterType.ENERGY);
if (energy > 0) {
int numActivations = energy / 3;
for (int i = 0; i < numActivations; i++) {
pumpedPower *= 2;
}
return (pumpedPower - initPower) * 15;
}
}
}
}
// default value
return 10;
}
protected int addValue(int value, String text) { protected int addValue(int value, String text) {
return value; return value;
} }

View File

@@ -4,15 +4,21 @@ import java.io.BufferedReader;
import java.io.InputStream; import java.io.InputStream;
import java.io.InputStreamReader; import java.io.InputStreamReader;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap; import java.util.EnumMap;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Set;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.StaticData; import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import forge.StaticData;
import forge.card.CardStateName; import forge.card.CardStateName;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
@@ -23,14 +29,21 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory; import forge.game.card.CardFactory;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.combat.CombatUtil;
import forge.game.event.GameEventAttackersDeclared;
import forge.game.event.GameEventCombatChanged;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType; import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone; import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.item.IPaperCard; import forge.item.IPaperCard;
import forge.item.PaperCard; import forge.item.PaperCard;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
public abstract class GameState { public abstract class GameState {
private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>(); private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>();
@@ -48,17 +61,38 @@ public abstract class GameState {
private String humanCounters = ""; private String humanCounters = "";
private String computerCounters = ""; private String computerCounters = "";
private boolean puzzleCreatorState = false;
private final Map<ZoneType, String> humanCardTexts = new EnumMap<ZoneType, String>(ZoneType.class); private final Map<ZoneType, String> humanCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
private final Map<ZoneType, String> aiCardTexts = new EnumMap<ZoneType, String>(ZoneType.class); private final Map<ZoneType, String> aiCardTexts = new EnumMap<ZoneType, String>(ZoneType.class);
private final Map<Integer, Card> idToCard = new HashMap<>(); private final Map<Integer, Card> idToCard = new HashMap<>();
private final Map<Card, Integer> cardToAttachId = new HashMap<>(); private final Map<Card, Integer> cardToAttachId = new HashMap<>();
private final Map<Card, Integer> markedDamage = new HashMap<>();
private final Map<Card, List<String>> cardToChosenClrs = new HashMap<>();
private final Map<Card, String> cardToChosenType = new HashMap<>();
private final Map<Card, List<String>> cardToRememberedId = new HashMap<>();
private final Map<Card, List<String>> cardToImprintedId = new HashMap<>();
private final Map<Card, String> cardToExiledWithId = new HashMap<>();
private final Map<Card, Card> cardAttackMap = new HashMap<>();
private final Map<Card, String> cardToScript = new HashMap<>();
private final Map<String, String> abilityString = new HashMap<>(); private final Map<String, String> abilityString = new HashMap<>();
private final Set<Card> cardsReferencedByID = new HashSet<>();
private String tChangePlayer = "NONE"; private String tChangePlayer = "NONE";
private String tChangePhase = "NONE"; private String tChangePhase = "NONE";
private String precastHuman = null;
private String precastAI = null;
// Targeting for precast spells in a game state (mostly used by Puzzle Mode game states)
private final int TARGET_NONE = -1; // untargeted spell (e.g. Joraga Invocation)
private final int TARGET_HUMAN = -2;
private final int TARGET_AI = -3;
public GameState() { public GameState() {
} }
@@ -67,18 +101,31 @@ public abstract class GameState {
@Override @Override
public String toString() { public String toString() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append(String.format("humanlife=%d\n", humanLife));
sb.append(String.format("ailife=%d\n", computerLife)); if (puzzleCreatorState) {
// append basic puzzle metadata if we're dumping from the puzzle creator screen
sb.append("[metadata]\n");
sb.append("Name:New Puzzle\n");
sb.append("URL:https://www.cardforge.org\n");
sb.append("Goal:Win\n");
sb.append("Turns:1\n");
sb.append("Difficulty:Easy\n");
sb.append("Description:Win this turn.\n");
sb.append("[state]\n");
}
sb.append(TextUtil.concatNoSpace("humanlife=", String.valueOf(humanLife), "\n"));
sb.append(TextUtil.concatNoSpace("ailife=", String.valueOf(computerLife), "\n"));
if (!humanCounters.isEmpty()) { if (!humanCounters.isEmpty()) {
sb.append(String.format("humancounters=%s\n", humanCounters)); sb.append(TextUtil.concatNoSpace("humancounters=", humanCounters, "\n"));
} }
if (!computerCounters.isEmpty()) { if (!computerCounters.isEmpty()) {
sb.append(String.format("aicounters=%s\n", computerCounters)); sb.append(TextUtil.concatNoSpace("aicounters=", computerCounters, "\n"));
} }
sb.append(String.format("activeplayer=%s\n", tChangePlayer)); sb.append(TextUtil.concatNoSpace("activeplayer=", tChangePlayer, "\n"));
sb.append(String.format("activephase=%s\n", tChangePhase)); sb.append(TextUtil.concatNoSpace("activephase=", tChangePhase, "\n"));
appendCards(humanCardTexts, "human", sb); appendCards(humanCardTexts, "human", sb);
appendCards(aiCardTexts, "ai", sb); appendCards(aiCardTexts, "ai", sb);
return sb.toString(); return sb.toString();
@@ -86,13 +133,13 @@ public abstract class GameState {
private void appendCards(Map<ZoneType, String> cardTexts, String categoryPrefix, StringBuilder sb) { private void appendCards(Map<ZoneType, String> cardTexts, String categoryPrefix, StringBuilder sb) {
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) { for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
sb.append(String.format("%s%s=%s\n", categoryPrefix, ZONES.get(kv.getKey()), kv.getValue())); sb.append(TextUtil.concatNoSpace(categoryPrefix, ZONES.get(kv.getKey()), "=", kv.getValue(), "\n"));
} }
} }
public void initFromGame(Game game) throws Exception { public void initFromGame(Game game) throws Exception {
FCollectionView<Player> players = game.getPlayers(); FCollectionView<Player> players = game.getPlayers();
// Can only serialized a two player game with one AI and one human. // Can only serialize a two player game with one AI and one human.
if (players.size() != 2) { if (players.size() != 2) {
throw new Exception("Game not supported"); throw new Exception("Game not supported");
} }
@@ -110,12 +157,52 @@ public abstract class GameState {
tChangePhase = game.getPhaseHandler().getPhase().toString(); tChangePhase = game.getPhaseHandler().getPhase().toString();
aiCardTexts.clear(); aiCardTexts.clear();
humanCardTexts.clear(); humanCardTexts.clear();
// Mark the cards that need their ID remembered for various reasons
cardsReferencedByID.clear();
for (ZoneType zone : ZONES.keySet()) {
for (Card card : game.getCardsIn(zone)) {
if (card.getExiledWith() != null) {
// Remember the ID of the card that exiled this card
cardsReferencedByID.add(card.getExiledWith());
}
if (zone == ZoneType.Battlefield) {
if (!card.getEnchantedBy(false).isEmpty()
|| !card.getEquippedBy(false).isEmpty()
|| !card.getFortifiedBy(false).isEmpty()) {
// Remember the ID of cards that have attachments
cardsReferencedByID.add(card);
}
}
for (Object o : card.getRemembered()) {
// Remember the IDs of remembered cards
if (o instanceof Card) {
cardsReferencedByID.add((Card)o);
}
}
for (Card i : card.getImprintedCards()) {
// Remember the IDs of imprinted cards
cardsReferencedByID.add(i);
}
if (game.getCombat() != null && game.getCombat().isAttacking(card)) {
// Remember the IDs of attacked planeswalkers
GameEntity def = game.getCombat().getDefenderByAttacker(card);
if (def instanceof Card) {
cardsReferencedByID.add((Card)def);
}
}
}
}
for (ZoneType zone : ZONES.keySet()) { for (ZoneType zone : ZONES.keySet()) {
// Init texts to empty, so that restoring will clear the state // Init texts to empty, so that restoring will clear the state
// if the zone had no cards in it (e.g. empty hand). // if the zone had no cards in it (e.g. empty hand).
aiCardTexts.put(zone, ""); aiCardTexts.put(zone, "");
humanCardTexts.put(zone, ""); humanCardTexts.put(zone, "");
for (Card card : game.getCardsIn(zone)) { for (Card card : game.getCardsIn(zone)) {
if (card.getName().equals("Puzzle Goal") && card.getOracleText().contains("New Puzzle")) {
puzzleCreatorState = true;
}
if (card instanceof DetachedCardEffect) { if (card instanceof DetachedCardEffect) {
continue; continue;
} }
@@ -140,6 +227,11 @@ public abstract class GameState {
if (c.isCommander()) { if (c.isCommander()) {
newText.append("|IsCommander"); newText.append("|IsCommander");
} }
if (cardsReferencedByID.contains(c)) {
newText.append("|Id:").append(c.getId());
}
if (zoneType == ZoneType.Battlefield) { if (zoneType == ZoneType.Battlefield) {
if (c.isTapped()) { if (c.isTapped()) {
newText.append("|Tapped"); newText.append("|Tapped");
@@ -147,6 +239,16 @@ public abstract class GameState {
if (c.isSick()) { if (c.isSick()) {
newText.append("|SummonSick"); newText.append("|SummonSick");
} }
if (c.isRenowned()) {
newText.append("|Renowned");
}
if (c.isMonstrous()) {
newText.append("|Monstrous:");
newText.append(c.getMonstrosityNum());
}
if (c.isPhasedOut()) {
newText.append("|PhasedOut");
}
if (c.isFaceDown()) { if (c.isFaceDown()) {
newText.append("|FaceDown"); newText.append("|FaceDown");
if (c.isManifested()) { if (c.isManifested()) {
@@ -155,11 +257,10 @@ public abstract class GameState {
} }
if (c.getCurrentStateName().equals(CardStateName.Transformed)) { if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed"); newText.append("|Transformed");
} } else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
Map<CounterType, Integer> counters = c.getCounters(); newText.append("|Flipped");
if (!counters.isEmpty()) { } else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Counters:"); newText.append("|Meld");
newText.append(countersToString(counters));
} }
if (c.getEquipping() != null) { if (c.getEquipping() != null) {
newText.append("|Attaching:").append(c.getEquipping().getId()); newText.append("|Attaching:").append(c.getEquipping().getId());
@@ -169,10 +270,66 @@ public abstract class GameState {
newText.append("|Attaching:").append(c.getEnchantingCard().getId()); newText.append("|Attaching:").append(c.getEnchantingCard().getId());
} }
if (!c.getEnchantedBy(false).isEmpty() || !c.getEquippedBy(false).isEmpty() || !c.getFortifiedBy(false).isEmpty()) { if (c.getDamage() > 0) {
newText.append("|Id:").append(c.getId()); newText.append("|Damage:").append(c.getDamage());
}
if (!c.getChosenColor().isEmpty()) {
newText.append("|ChosenColor:").append(TextUtil.join(c.getChosenColors(), ","));
}
if (!c.getChosenType().isEmpty()) {
newText.append("|ChosenType:").append(c.getChosenType());
}
List<String> rememberedCardIds = Lists.newArrayList();
for (Object obj : c.getRemembered()) {
if (obj instanceof Card) {
int id = ((Card)obj).getId();
rememberedCardIds.add(String.valueOf(id));
}
}
if (!rememberedCardIds.isEmpty()) {
newText.append("|RememberedCards:").append(TextUtil.join(rememberedCardIds, ","));
}
List<String> imprintedCardIds = Lists.newArrayList();
for (Card impr : c.getImprintedCards()) {
int id = impr.getId();
imprintedCardIds.add(String.valueOf(id));
}
if (!imprintedCardIds.isEmpty()) {
newText.append("|Imprinting:").append(TextUtil.join(imprintedCardIds, ","));
} }
} }
if (zoneType == ZoneType.Exile) {
if (c.getExiledWith() != null) {
newText.append("|ExiledWith:").append(c.getExiledWith().getId());
}
if (c.isFaceDown()) {
newText.append("|FaceDown"); // Exiled face down
}
}
if (zoneType == ZoneType.Battlefield || zoneType == ZoneType.Exile) {
// A card can have counters on the battlefield and in exile (e.g. exiled by Mairsil, the Pretender)
Map<CounterType, Integer> counters = c.getCounters();
if (!counters.isEmpty()) {
newText.append("|Counters:");
newText.append(countersToString(counters));
}
}
if (c.getGame().getCombat() != null) {
if (c.getGame().getCombat().isAttacking(c)) {
newText.append("|Attacking");
GameEntity def = c.getGame().getCombat().getDefenderByAttacker(c);
if (def instanceof Card) {
newText.append(":" + def.getId());
}
}
}
cardTexts.put(zoneType, newText.toString()); cardTexts.put(zoneType, newText.toString());
} }
@@ -186,7 +343,7 @@ public abstract class GameState {
} }
first = false; first = false;
counterString.append(String.format("%s=%d", kv.getKey().toString(), kv.getValue())); counterString.append(TextUtil.concatNoSpace(kv.getKey().toString(), "=", String.valueOf(kv.getValue())));
} }
return counterString.toString(); return counterString.toString();
} }
@@ -298,6 +455,12 @@ public abstract class GameState {
abilityString.put(categoryName.substring("ability".length()), categoryValue); abilityString.put(categoryName.substring("ability".length()), categoryValue);
} }
else if (categoryName.endsWith("precast")) {
if (isHuman)
precastHuman = categoryValue;
else
precastAI = categoryValue;
}
else { else {
System.out.println("Unknown key: " + categoryName); System.out.println("Unknown key: " + categoryName);
} }
@@ -318,6 +481,13 @@ public abstract class GameState {
idToCard.clear(); idToCard.clear();
cardToAttachId.clear(); cardToAttachId.clear();
cardToRememberedId.clear();
cardToExiledWithId.clear();
markedDamage.clear();
cardToChosenClrs.clear();
cardToChosenType.clear();
cardToScript.clear();
cardAttackMap.clear();
Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null; Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null;
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase); PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
@@ -331,22 +501,363 @@ public abstract class GameState {
if (!computerCounters.isEmpty()) { if (!computerCounters.isEmpty()) {
applyCountersToGameEntity(ai, computerCounters); applyCountersToGameEntity(ai, computerCounters);
} }
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn); game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); game.getTriggerHandler().setSuppressAllTriggers(true);
setupPlayerState(humanLife, humanCardTexts, human); setupPlayerState(humanLife, humanCardTexts, human);
setupPlayerState(computerLife, aiCardTexts, ai); setupPlayerState(computerLife, aiCardTexts, ai);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); handleCardAttachments();
handleChosenEntities();
handleRememberedEntities();
handleScriptExecution(game);
handlePrecastSpells(game);
handleMarkedDamage();
game.getTriggerHandler().setSuppressAllTriggers(false);
// Combat only works for 1v1 matches for now (which are the only matches dev mode supports anyway)
// Note: triggers may fire during combat declarations ("whenever X attacks, ...", etc.)
if (newPhase == PhaseType.COMBAT_DECLARE_ATTACKERS || newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS) {
boolean toDeclareBlockers = newPhase == PhaseType.COMBAT_DECLARE_BLOCKERS;
handleCombat(game, newPlayerTurn, newPlayerTurn.getSingleOpponent(), toDeclareBlockers);
}
game.getStack().setResolving(false); game.getStack().setResolving(false);
game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated
} }
private void handleCombat(final Game game, final Player attackingPlayer, final Player defendingPlayer, final boolean toDeclareBlockers) {
// First we need to ensure that all attackers are declared in the Declare Attackers step,
// even if proceeding straight to Declare Blockers
game.getPhaseHandler().devModeSet(PhaseType.COMBAT_DECLARE_ATTACKERS, attackingPlayer);
if (game.getPhaseHandler().getCombat() == null) {
game.getPhaseHandler().setCombat(new Combat(attackingPlayer));
game.updateCombatForView();
}
Combat combat = game.getPhaseHandler().getCombat();
for (Entry<Card, Card> attackMap : cardAttackMap.entrySet()) {
Card attacker = attackMap.getKey();
Card attacked = attackMap.getValue();
combat.addAttacker(attacker, attacked == null ? defendingPlayer : attacked);
}
// Run the necessary combat events and triggers to set things up correctly as if the
// attack was actually declared by the attacking player
Multimap<GameEntity, Card> attackersMap = ArrayListMultimap.create();
for (GameEntity ge : combat.getDefenders()) {
attackersMap.putAll(ge, combat.getAttackersOf(ge));
}
game.fireEvent(new GameEventAttackersDeclared(attackingPlayer, attackersMap));
if (!combat.getAttackers().isEmpty()) {
List<GameEntity> attackedTarget = Lists.newArrayList();
for (final Card c : combat.getAttackers()) {
attackedTarget.add(combat.getDefenderByAttacker(c));
}
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Attackers", combat.getAttackers());
runParams.put("AttackingPlayer", combat.getAttackingPlayer());
runParams.put("AttackedTarget", attackedTarget);
game.getTriggerHandler().runTrigger(TriggerType.AttackersDeclared, runParams, false);
}
for (final Card c : combat.getAttackers()) {
CombatUtil.checkDeclaredAttacker(game, c, combat);
}
game.getTriggerHandler().resetActiveTriggers();
game.updateCombatForView();
game.fireEvent(new GameEventCombatChanged());
// Gracefully proceed to Declare Blockers, giving priority to the defending player,
// but only if the stack is empty (otherwise the game will crash).
game.getStack().addAllTriggeredAbilitiesToStack();
if (toDeclareBlockers && game.getStack().isEmpty()) {
game.getPhaseHandler().devAdvanceToPhase(PhaseType.COMBAT_DECLARE_BLOCKERS);
}
}
private void handleRememberedEntities() {
// Remembered: X
for (Entry<Card, List<String>> rememberedEnts : cardToRememberedId.entrySet()) {
Card c = rememberedEnts.getKey();
List<String> ids = rememberedEnts.getValue();
for (String id : ids) {
Card tgt = idToCard.get(Integer.parseInt(id));
c.addRemembered(tgt);
}
}
// Imprinting: X
for (Entry<Card, List<String>> imprintedCards : cardToImprintedId.entrySet()) {
Card c = imprintedCards.getKey();
List<String> ids = imprintedCards.getValue();
for (String id : ids) {
Card tgt = idToCard.get(Integer.parseInt(id));
c.addImprintedCard(tgt);
}
}
// Exiled with X
for (Entry<Card, String> rememberedEnts : cardToExiledWithId.entrySet()) {
Card c = rememberedEnts.getKey();
String id = rememberedEnts.getValue();
Card exiledWith = idToCard.get(Integer.parseInt(id));
c.setExiledWith(exiledWith);
}
}
private int parseTargetInScript(final String tgtDef) {
int tgtID = TARGET_NONE;
if (tgtDef.equalsIgnoreCase("human")) {
tgtID = TARGET_HUMAN;
} else if (tgtDef.equalsIgnoreCase("ai")) {
tgtID = TARGET_AI;
} else {
tgtID = Integer.parseInt(tgtDef);
}
return tgtID;
}
private void handleScriptedTargetingForSA(final Game game, final SpellAbility sa, int tgtID) {
Player human = game.getPlayers().get(0);
Player ai = game.getPlayers().get(1);
if (tgtID != TARGET_NONE) {
switch (tgtID) {
case TARGET_HUMAN:
sa.getTargets().add(human);
break;
case TARGET_AI:
sa.getTargets().add(ai);
break;
default:
sa.getTargets().add(idToCard.get(tgtID));
break;
}
}
}
private void handleScriptExecution(final Game game) {
for (Entry<Card, String> scriptPtr : cardToScript.entrySet()) {
Card c = scriptPtr.getKey();
String sPtr = scriptPtr.getValue();
executeScript(game, c, sPtr);
}
}
private void executeScript(Game game, Card c, String sPtr) {
int tgtID = TARGET_NONE;
if (sPtr.contains("->")) {
String tgtDef = sPtr.substring(sPtr.lastIndexOf("->") + 2);
tgtID = parseTargetInScript(tgtDef);
sPtr = sPtr.substring(0, sPtr.lastIndexOf("->"));
}
SpellAbility sa = null;
if (StringUtils.isNumeric(sPtr)) {
int numSA = Integer.parseInt(sPtr);
if (c.getSpellAbilities().size() >= numSA) {
sa = c.getSpellAbilities().get(numSA);
} else {
System.err.println("ERROR: Unable to find SA with index " + numSA + " on card " + c + " to execute!");
}
} else {
// Special handling for keyworded abilities
if (sPtr.startsWith("KW#")) {
String kwName = sPtr.substring(3);
FCollectionView<SpellAbility> saList = c.getSpellAbilities();
if (kwName.equals("Awaken") || kwName.equals("AwakenOnly")) {
// AwakenOnly only creates the Awaken effect, while Awaken precasts the whole spell with Awaken
for (SpellAbility ab : saList) {
if (ab.getDescription().startsWith("Awaken")) {
ab.setActivatingPlayer(c.getController());
ab.getSubAbility().setActivatingPlayer(c.getController());
// target for Awaken is set in its first subability
handleScriptedTargetingForSA(game, ab.getSubAbility(), tgtID);
sa = kwName.equals("AwakenOnly") ? ab.getSubAbility() : ab;
}
}
if (sa == null) {
System.err.println("ERROR: Could not locate keyworded ability Awaken in card " + c + " to execute!");
return;
}
}
} else {
// SVar-based script execution
String svarValue = "";
if (sPtr.startsWith("CustomScript:")) {
// A custom line defined in the game state file
svarValue = sPtr.substring(sPtr.indexOf(":") + 1);
} else {
// A SVar from the card script file
if (!c.hasSVar(sPtr)) {
System.err.println("ERROR: Unable to find SVar " + sPtr + " on card " + c + " + to execute!");
return;
}
svarValue = c.getSVar(sPtr);
if (tgtID != TARGET_NONE && svarValue.contains("| Defined$")) {
// We want a specific target, so try to undefine a predefined target if possible
svarValue = TextUtil.fastReplace(svarValue, "| Defined$", "| Undefined$");
if (tgtID == TARGET_HUMAN || tgtID == TARGET_AI) {
svarValue += " | ValidTgts$ Player";
} else {
svarValue += " | ValidTgts$ Card";
}
}
}
sa = AbilityFactory.getAbility(svarValue, c);
if (sa == null) {
System.err.println("ERROR: Unable to generate ability for SVar " + svarValue);
}
}
}
sa.setActivatingPlayer(c.getController());
handleScriptedTargetingForSA(game, sa, tgtID);
sa.resolve();
// resolve subabilities
SpellAbility subSa = sa.getSubAbility();
while (subSa != null) {
subSa.resolve();
subSa = subSa.getSubAbility();
}
}
private void handlePrecastSpells(final Game game) {
Player human = game.getPlayers().get(0);
Player ai = game.getPlayers().get(1);
if (precastHuman != null) {
String[] spellList = TextUtil.split(precastHuman, ';');
for (String spell : spellList) {
precastSpellFromCard(spell, human, game);
}
}
if (precastAI != null) {
String[] spellList = TextUtil.split(precastAI, ';');
for (String spell : spellList) {
precastSpellFromCard(spell, ai, game);
}
}
}
private void precastSpellFromCard(String spellDef, final Player activator, final Game game) {
int tgtID = TARGET_NONE;
String scriptID = "";
if (spellDef.contains(":")) {
// targeting via -> will be handled in executeScript
scriptID = spellDef.substring(spellDef.indexOf(":") + 1);
spellDef = spellDef.substring(0, spellDef.indexOf(":"));
} else if (spellDef.contains("->")) {
String tgtDef = spellDef.substring(spellDef.indexOf("->") + 2);
tgtID = parseTargetInScript(tgtDef);
spellDef = spellDef.substring(0, spellDef.indexOf("->"));
}
PaperCard pc = StaticData.instance().getCommonCards().getCard(spellDef);
if (pc == null) {
System.err.println("ERROR: Could not find a card with name " + spellDef + " to precast!");
return;
}
Card c = Card.fromPaperCard(pc, activator);
SpellAbility sa = null;
if (!scriptID.isEmpty()) {
executeScript(game, c, scriptID);
return;
}
sa = c.getFirstSpellAbility();
sa.setActivatingPlayer(activator);
handleScriptedTargetingForSA(game, sa, tgtID);
sa.resolve();
}
private void handleMarkedDamage() {
for (Entry<Card, Integer> entry : markedDamage.entrySet()) {
Card c = entry.getKey();
Integer dmg = entry.getValue();
c.setDamage(dmg);
}
}
private void handleChosenEntities() {
// TODO: the AI still gets to choose something (and the notification box pops up) before the
// choice is overwritten here. Somehow improve this so that there is at least no notification
// about the choice that will be force-changed anyway.
// Chosen colors
for (Entry<Card, List<String>> entry : cardToChosenClrs.entrySet()) {
Card c = entry.getKey();
List<String> colors = entry.getValue();
c.setChosenColors(colors);
}
// Chosen type
for (Entry<Card, String> entry : cardToChosenType.entrySet()) {
Card c = entry.getKey();
c.setChosenType(entry.getValue());
}
}
private void handleCardAttachments() {
// Unattach all permanents first
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
attachedTo.unEnchantAllCards();
attachedTo.unEquipAllCards();
for (Card c : attachedTo.getFortifiedBy(true)) {
attachedTo.unFortifyCard(c);
}
}
// Attach permanents by ID
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
Card attacher = entry.getKey();
if (attacher.isEquipment()) {
attacher.equipCard(attachedTo);
} else if (attacher.isAura()) {
attacher.enchantEntity(attachedTo);
} else if (attacher.isFortified()) {
attacher.fortifyCard(attachedTo);
}
}
}
private void applyCountersToGameEntity(GameEntity entity, String counterString) { private void applyCountersToGameEntity(GameEntity entity, String counterString) {
//entity.setCounters(new HashMap<CounterType, Integer>()); entity.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
String[] allCounterStrings = counterString.split(","); String[] allCounterStrings = counterString.split(",");
for (final String counterPair : allCounterStrings) { for (final String counterPair : allCounterStrings) {
String[] pair = counterPair.split("=", 2); String[] pair = counterPair.split("=", 2);
@@ -356,7 +867,6 @@ public abstract class GameState {
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) { private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
// Lock check static as we setup player state // Lock check static as we setup player state
final Game game = p.getGame();
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class); Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) { for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
@@ -384,9 +894,13 @@ public abstract class GameState {
Map<CounterType, Integer> counters = c.getCounters(); Map<CounterType, Integer> counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters // Note: Not clearCounters() since we want to keep the counters
// var as-is. // var as-is.
c.setCounters(new HashMap<CounterType, Integer>()); c.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
p.getZone(ZoneType.Hand).add(c); p.getZone(ZoneType.Hand).add(c);
if (c.isAura()) { if (c.isAura()) {
// dummy "enchanting" to indicate that the card will be force-attached elsewhere
// (will be overridden later, so the actual value shouldn't matter)
c.setEnchanting(c);
p.getGame().getAction().moveToPlay(c, null); p.getGame().getAction().moveToPlay(c, null);
} else { } else {
p.getGame().getAction().moveToPlay(c, null); p.getGame().getAction().moveToPlay(c, null);
@@ -401,25 +915,6 @@ public abstract class GameState {
} }
} }
game.getTriggerHandler().suppressMode(TriggerType.Unequip);
for(Entry<Card, Integer> entry : cardToAttachId.entrySet()) {
Card attachedTo = idToCard.get(entry.getValue());
Card attacher = entry.getKey();
attachedTo.unEnchantAllCards();
attachedTo.unEquipAllCards();
if (attacher.isEquipment()) {
attacher.equipCard(attachedTo);
} else if (attacher.isAura()) {
attacher.enchantEntity(attachedTo);
} else if (attacher.isFortified()) {
attacher.fortifyCard(attachedTo);
}
}
game.getTriggerHandler().clearSuppression(TriggerType.Unequip);
} }
/** /**
@@ -453,6 +948,11 @@ public abstract class GameState {
c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player); c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player);
} else { } else {
PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode); PaperCard pc = StaticData.instance().getCommonCards().getCard(cardinfo[0], setCode);
if (pc == null) {
System.err.println("ERROR: Tried to create a non-existent card named " + cardinfo[0] + " (set: " + (setCode == null ? "any" : setCode) + ") when loading game state!");
continue;
}
c = Card.fromPaperCard(pc, player); c = Card.fromPaperCard(pc, player);
if (setCode != null) { if (setCode != null) {
hasSetCurSet = true; hasSetCurSet = true;
@@ -463,6 +963,13 @@ public abstract class GameState {
for (final String info : cardinfo) { for (final String info : cardinfo) {
if (info.startsWith("Tapped")) { if (info.startsWith("Tapped")) {
c.tap(); c.tap();
} else if (info.startsWith("Renowned")) {
c.setRenowned(true);
} else if (info.startsWith("Monstrous:")) {
c.setMonstrous(true);
c.setMonstrosityNum(Integer.parseInt(info.substring((info.indexOf(':') + 1))));
} else if (info.startsWith("PhasedOut")) {
c.setPhasedOut(true);
} else if (info.startsWith("Counters:")) { } else if (info.startsWith("Counters:")) {
applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1)); applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("SummonSick")) { } else if (info.startsWith("SummonSick")) {
@@ -474,6 +981,10 @@ public abstract class GameState {
} }
} else if (info.startsWith("Transformed")) { } else if (info.startsWith("Transformed")) {
c.setState(CardStateName.Transformed, true); c.setState(CardStateName.Transformed, true);
} else if (info.startsWith("Flipped")) {
c.setState(CardStateName.Flipped, true);
} else if (info.startsWith("Meld")) {
c.setState(CardStateName.Meld, true);
} else if (info.startsWith("IsCommander")) { } else if (info.startsWith("IsCommander")) {
// TODO: This doesn't seem to properly restore the ability to play the commander. Why? // TODO: This doesn't seem to properly restore the ability to play the commander. Why?
c.setCommander(true); c.setCommander(true);
@@ -488,6 +999,28 @@ public abstract class GameState {
} else if (info.startsWith("Ability:")) { } else if (info.startsWith("Ability:")) {
String abString = info.substring(info.indexOf(':') + 1).toLowerCase(); String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c)); c.addSpellAbility(AbilityFactory.getAbility(abilityString.get(abString), c));
} else if (info.startsWith("Damage:")) {
int dmg = Integer.parseInt(info.substring(info.indexOf(':') + 1));
markedDamage.put(c, dmg);
} else if (info.startsWith("ChosenColor:")) {
cardToChosenClrs.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ChosenType:")) {
cardToChosenType.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("ExecuteScript:")) {
cardToScript.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("RememberedCards:")) {
cardToRememberedId.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("Imprinting:")) {
cardToImprintedId.put(c, Arrays.asList(info.substring(info.indexOf(':') + 1).split(",")));
} else if (info.startsWith("ExiledWith:")) {
cardToExiledWithId.put(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("Attacking")) {
if (info.contains(":")) {
int id = Integer.parseInt(info.substring(info.indexOf(':') + 1));
cardAttackMap.put(c, idToCard.get(id));
} else {
cardAttackMap.put(c, null);
}
} }
} }

View File

@@ -2,7 +2,6 @@ package forge.ai;
import java.util.Set; import java.util.Set;
import forge.AIOption;
import forge.LobbyPlayer; import forge.LobbyPlayer;
import forge.game.Game; import forge.game.Game;
import forge.game.player.IGameEntitiesFactory; import forge.game.player.IGameEntitiesFactory;

View File

@@ -1,12 +1,5 @@
package forge.ai; package forge.ai;
import java.security.InvalidParameterException;
import java.util.*;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import com.esotericsoftware.minlog.Log; import com.esotericsoftware.minlog.Log;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
@@ -14,9 +7,9 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.ListMultimap; import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Multimap; import com.google.common.collect.Multimap;
import forge.LobbyPlayer; import forge.LobbyPlayer;
import forge.ai.ability.ProtectAi; import forge.ai.ability.ProtectAi;
import forge.card.CardStateName;
import forge.card.ColorSet; import forge.card.ColorSet;
import forge.card.ICardFace; import forge.card.ICardFace;
import forge.card.MagicColor; import forge.card.MagicColor;
@@ -39,14 +32,9 @@ import forge.game.mana.Mana;
import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.DelayedReveal; import forge.game.player.*;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.player.PlayerController;
import forge.game.player.PlayerView;
import forge.game.replacement.ReplacementEffect; import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.*; import forge.game.spellability.*;
import forge.game.trigger.Trigger;
import forge.game.trigger.WrappedAbility; import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.item.PaperCard; import forge.item.PaperCard;
@@ -55,6 +43,12 @@ import forge.util.ITriggerEvent;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.collect.FCollection; import forge.util.collect.FCollection;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
import java.security.InvalidParameterException;
import java.util.*;
/** /**
@@ -115,7 +109,23 @@ public class PlayerControllerAi extends PlayerController {
if (ability.getApi() != null) { if (ability.getApi() != null) {
switch (ability.getApi()) { switch (ability.getApi()) {
case ChooseNumber: case ChooseNumber:
return ability.getActivatingPlayer().isOpponentOf(player) ? 0 : ComputerUtilMana.determineLeftoverMana(ability, player); Player payingPlayer = ability.getActivatingPlayer();
String logic = ability.getParamOrDefault("AILogic", "");
boolean anyController = logic.equals("MaxForAnyController");
if (logic.startsWith("PowerLeakMaxMana.") && ability.getHostCard().isEnchantingCard()) {
// For cards like Power Leak, the payer will be the owner of the enchanted card
// TODO: is there any way to generalize this and avoid a special exclusion?
payingPlayer = ability.getHostCard().getEnchantingCard().getController();
}
int number = ComputerUtilMana.determineLeftoverMana(ability, player);
if (logic.startsWith("MaxMana.") || logic.startsWith("PowerLeakMaxMana.")) {
number = Math.min(number, Integer.parseInt(logic.substring(logic.indexOf(".") + 1)));
}
return payingPlayer.isOpponentOf(player) && !anyController ? 0 : number;
case BidLife: case BidLife:
return 0; return 0;
default: default:
@@ -179,8 +189,8 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) { public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
final SpellAbility sa = wrapper.getWrappedAbility(); final SpellAbility sa = wrapper.getWrappedAbility();
final Trigger regtrig = wrapper.getTrigger(); //final Trigger regtrig = wrapper.getTrigger();
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) { if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true; return true;
} }
@@ -282,8 +292,43 @@ public class PlayerControllerAi extends PlayerController {
} }
@Override @Override
public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone) { public CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone, SpellAbility source) {
//TODO Add logic for AI ordering here //TODO Add more logic for AI ordering here
// In presence of Volrath's Shapeshifter in deck, try to place the best creature on top of the graveyard
if (destinationZone == ZoneType.Graveyard) {
if (!CardLists.filter(game.getCardsInGame(), new Predicate<Card>() {
@Override
public boolean apply(Card card) {
// need a custom predicate here since Volrath's Shapeshifter may have a different name OTB
return card.getName().equals("Volrath's Shapeshifter")
|| card.getStates().contains(CardStateName.OriginalText) && card.getState(CardStateName.OriginalText).getName().equals("Volrath's Shapeshifter");
}
}).isEmpty()) {
int bestValue = 0;
Card bestCreature = null;
for (Card c : cards) {
int curValue = ComputerUtilCard.evaluateCreature(c);
if (c.isCreature() && curValue > bestValue) {
bestValue = curValue;
bestCreature = c;
}
}
if (bestCreature != null) {
CardCollection reordered = new CardCollection();
for (Card c : cards) {
if (!c.equals(bestCreature)) {
reordered.add(c);
}
}
reordered.add(bestCreature);
return reordered;
}
}
}
// Default: return with the same order as was passed into this method
return cards; return cards;
} }
@@ -844,21 +889,40 @@ public class PlayerControllerAi extends PlayerController {
@Override @Override
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) { public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
if (sa.hasParam("AILogic")) { if (sa.hasParam("AILogic")) {
CardCollectionView aiLibrary = player.getCardsIn(ZoneType.Library);
CardCollectionView oppLibrary = ComputerUtil.getOpponentFor(player).getCardsIn(ZoneType.Library);
final Card source = sa.getHostCard();
final String logic = sa.getParam("AILogic"); final String logic = sa.getParam("AILogic");
if (source != null && source.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
// If any Conspiracies are present, try not to choose the same name twice
// (otherwise the AI will spam the same name)
for (Card consp : player.getCardsIn(ZoneType.Command)) {
if (consp.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")) {
String chosenName = consp.getNamedCard();
if (!chosenName.isEmpty()) {
aiLibrary = CardLists.filter(aiLibrary, Predicates.not(CardPredicates.nameEquals(chosenName)));
}
}
}
}
if (logic.equals("MostProminentInComputerDeck")) { if (logic.equals("MostProminentInComputerDeck")) {
return ComputerUtilCard.getMostProminentCardName(player.getCardsIn(ZoneType.Library)); return ComputerUtilCard.getMostProminentCardName(aiLibrary);
} else if (logic.equals("MostProminentInHumanDeck")) { } else if (logic.equals("MostProminentInHumanDeck")) {
return ComputerUtilCard.getMostProminentCardName(player.getOpponent().getCardsIn(ZoneType.Library)); return ComputerUtilCard.getMostProminentCardName(oppLibrary);
} else if (logic.equals("MostProminentCreatureInComputerDeck")) { } else if (logic.equals("MostProminentCreatureInComputerDeck")) {
CardCollectionView cards = CardLists.getValidCards(player.getCardsIn(ZoneType.Library), "Creature", player, sa.getHostCard()); CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Creature", player, sa.getHostCard());
return ComputerUtilCard.getMostProminentCardName(cards); return ComputerUtilCard.getMostProminentCardName(cards);
} else if (logic.equals("BestCreatureInComputerDeck")) { } else if (logic.equals("BestCreatureInComputerDeck")) {
return ComputerUtilCard.getBestCreatureAI(player.getCardsIn(ZoneType.Library)).getName(); return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName();
} else if (logic.equals("RandomInComputerDeck")) { } else if (logic.equals("RandomInComputerDeck")) {
return Aggregates.random(player.getCardsIn(ZoneType.Library)).getName(); return Aggregates.random(aiLibrary).getName();
} else if (logic.equals("MostProminentSpellInComputerDeck")) { } else if (logic.equals("MostProminentSpellInComputerDeck")) {
CardCollectionView cards = CardLists.getValidCards(player.getCardsIn(ZoneType.Library), "Card.Instant,Card.Sorcery", player, sa.getHostCard()); CardCollectionView cards = CardLists.getValidCards(aiLibrary, "Card.Instant,Card.Sorcery", player, sa.getHostCard());
return ComputerUtilCard.getMostProminentCardName(cards); return ComputerUtilCard.getMostProminentCardName(cards);
} else if (logic.equals("CursedScroll")) {
return SpecialCardAi.CursedScroll.chooseCard(player, sa);
} }
} else { } else {
CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents()); CardCollectionView list = CardLists.filterControlledBy(game.getCardsInGame(), player.getOpponents());

View File

@@ -17,28 +17,20 @@
*/ */
package forge.ai; package forge.ai;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.card.ColorSet; import forge.card.ColorSet;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection; import forge.game.combat.Combat;
import forge.game.card.CardCollectionView; import forge.game.combat.CombatUtil;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.mana.ManaCostBeingPaid; import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
@@ -46,8 +38,18 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.staticability.StaticAbility;
import forge.game.trigger.Trigger;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import forge.util.TextUtil;
import forge.util.maps.LinkedHashMapToAmount;
import forge.util.maps.MapToAmount;
import org.apache.commons.lang3.tuple.Pair;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/** /**
* Special logic for individual cards * Special logic for individual cards
@@ -73,8 +75,8 @@ public class SpecialCardAi {
// Black Lotus and Lotus Bloom // Black Lotus and Lotus Bloom
public static class BlackLotus { public static class BlackLotus {
public static boolean consider(Player ai, SpellAbility sa, ManaCostBeingPaid cost) { public static boolean consider(final Player ai, final SpellAbility sa, final ManaCostBeingPaid cost) {
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true); CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
int numManaSrcs = manaSources.size(); int numManaSrcs = manaSources.size();
CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.Presets.NON_TOKEN, CardCollection allCards = CardLists.filter(ai.getAllCards(), Arrays.asList(CardPredicates.Presets.NON_TOKEN,
@@ -102,7 +104,7 @@ public class SpecialCardAi {
// Bonds of Faith // Bonds of Faith
public static class BondsOfFaith { public static class BondsOfFaith {
public static Card getBestAttachTarget(final Player ai, SpellAbility sa, List<Card> list) { public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
Card chosen = null; Card chosen = null;
List<Card> aiHumans = CardLists.filter(list, new Predicate<Card>() { List<Card> aiHumans = CardLists.filter(list, new Predicate<Card>() {
@@ -140,9 +142,25 @@ public class SpecialCardAi {
} }
} }
// Chain of Acid
public static class ChainOfAcid {
public static boolean consider(final Player ai, final SpellAbility sa) {
List<Card> AiLandsOnly = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.Presets.LANDS);
List<Card> OppPerms = CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Battlefield),
Predicates.not(CardPredicates.Presets.CREATURES));
// TODO: improve this logic (currently the AI has difficulty evaluating non-creature permanents,
// which it can only distinguish by their CMC, considering >CMC higher value).
// Currently ensures that the AI will still have lands provided that the human player goes to
// destroy all the AI's lands in order (to avoid manalock).
return !OppPerms.isEmpty() && AiLandsOnly.size() > OppPerms.size() + 2;
}
}
// Chain of Smog // Chain of Smog
public static class ChainOfSmog { public static class ChainOfSmog {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
if (ai.getCardsIn(ZoneType.Hand).isEmpty()) { if (ai.getCardsIn(ZoneType.Hand).isEmpty()) {
// to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point) // to avoid failure to add to stack, provide a legal target opponent first (choosing random at this point)
// TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a // TODO: this makes the AI target opponents with 0 cards in hand, but bailing from here causes a
@@ -165,11 +183,66 @@ public class SpecialCardAi {
} }
} }
// Cursed Scroll
public static class CursedScroll {
public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollectionView hand = ai.getCardsIn(ZoneType.Hand);
if (hand.isEmpty()) {
return false;
}
// For now, see if all cards in hand have the same name, and then proceed if true
return CardLists.filter(hand, CardPredicates.nameEquals(hand.getFirst().getName())).size() == hand.size();
}
public static String chooseCard(final Player ai, final SpellAbility sa) {
int maxCount = 0;
Card best = null;
CardCollectionView hand = ai.getCardsIn(ZoneType.Hand);
for (Card c : ai.getCardsIn(ZoneType.Hand)) {
int count = CardLists.filter(hand, CardPredicates.nameEquals(c.getName())).size();
if (count > maxCount) {
maxCount = count;
best = c;
}
}
return best.getName();
}
}
// Deathgorge Scavenger
public static class DeathgorgeScavenger {
public static boolean consider(final Player ai, final SpellAbility sa) {
Card worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
Card worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getOpponents().getCardsIn(ZoneType.Graveyard), Predicates.not(CardPredicates.Presets.CREATURES)));
if (worstCreat == null) {
worstCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), CardPredicates.Presets.CREATURES));
}
if (worstNonCreat == null) {
worstNonCreat = ComputerUtilCard.getWorstAI(CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Predicates.not(CardPredicates.Presets.CREATURES)));
}
sa.resetTargets();
if (worstCreat != null && ai.getLife() <= ai.getStartingLife() / 4) {
sa.getTargets().add(worstCreat);
} else if (worstNonCreat != null && ai.getGame().getCombat() != null
&& ai.getGame().getCombat().isAttacking(sa.getHostCard())) {
sa.getTargets().add(worstNonCreat);
} else if (worstCreat != null) {
sa.getTargets().add(worstCreat);
}
return sa.getTargets().getNumTargeted() > 0;
}
}
// Desecration Demon // Desecration Demon
public static class DesecrationDemon { public static class DesecrationDemon {
private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer private static final int demonSacThreshold = Integer.MAX_VALUE; // if we're in dire conditions, sac everything from worst to best hoping to find an answer
public static boolean considerSacrificingCreature(Player ai, SpellAbility sa) { public static boolean considerSacrificingCreature(final Player ai, final SpellAbility sa) {
CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach")))); CardCollection flyingCreatures = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), Predicates.and(CardPredicates.Presets.UNTAPPED, Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"))));
boolean hasUsefulBlocker = false; boolean hasUsefulBlocker = false;
@@ -193,7 +266,7 @@ public class SpecialCardAi {
// Donate // Donate
public static class Donate { public static class Donate {
public static boolean considerTargetingOpponent(Player ai, SpellAbility sa) { public static boolean considerTargetingOpponent(final Player ai, final SpellAbility sa) {
final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter( final Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(
ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) { if (donateTarget != null) {
@@ -229,7 +302,7 @@ public class SpecialCardAi {
return false; return false;
} }
public static boolean considerDonatingPermanent(Player ai, SpellAbility sa) { public static boolean considerDonatingPermanent(final Player ai, final SpellAbility sa) {
Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe"))); Card donateTarget = ComputerUtil.getCardPreference(ai, sa.getHostCard(), "DonateMe", CardLists.filter(ai.getCardsIn(ZoneType.Battlefield).threadSafeIterable(), CardPredicates.hasSVar("DonateMe")));
if (donateTarget != null) { if (donateTarget != null) {
sa.resetTargets(); sa.resetTargets();
@@ -243,9 +316,159 @@ public class SpecialCardAi {
} }
} }
// Electrostatic Pummeler
public static class ElectrostaticPummeler {
public static boolean consider(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
Game game = ai.getGame();
Combat combat = game.getCombat();
Pair<Integer, Integer> predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness());
// Try to save the Pummeler from death by pumping it if it's threatened with a damage spell
if (ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source)) {
SpellAbility saTop = game.getStack().peekAbility();
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop);
if (source.getNetToughness() - source.getDamage() <= dmg && predictedPT.getRight() - source.getDamage() > dmg)
return true;
}
}
// Do not activate if damage will be prevented
if (source.staticDamagePrevention(predictedPT.getLeft(), source, true, true) == 0) {
return false;
}
// Activate Electrostatic Pummeler's pump only as a combat trick
if (game.getPhaseHandler().is(PhaseType.COMBAT_BEGIN)) {
if (predictOverwhelmingDamage(ai, sa)) {
// We'll try to deal lethal trample/unblocked damage, so remember the card for attack
// and wait until declare blockers step.
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return false;
}
} else if (!game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false;
}
if (combat == null || !(combat.isAttacking(source) || combat.isBlocking(source))) {
return false;
}
boolean isBlocking = combat.isBlocking(source);
boolean cantDie = ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, source);
CardCollection opposition = isBlocking ? combat.getAttackersBlockedBy(source) : combat.getBlockers(source);
int oppP = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetAttack);
int oppT = Aggregates.sum(opposition, CardPredicates.Accessors.fnGetNetToughness);
boolean oppHasFirstStrike = false;
boolean oppCantDie = true;
boolean unblocked = opposition.isEmpty();
boolean canTrample = source.hasKeyword("Trample");
if (!isBlocking && combat.getDefenderByAttacker(source) instanceof Card) {
int loyalty = ((Card)combat.getDefenderByAttacker(source)).getCounters(CounterType.LOYALTY);
int totalDamageToPW = 0;
for (Card atk : (combat.getAttackersOf(combat.getDefenderByAttacker(source)))) {
if (combat.isUnblocked(atk)) {
totalDamageToPW += atk.getNetCombatDamage();
}
}
if (totalDamageToPW >= oppT + loyalty) {
// Already enough damage to take care of the planeswalker
return false;
}
if ((unblocked || canTrample) && predictedPT.getLeft() >= oppT + loyalty) {
// Can pump to kill the planeswalker, go for it
return true;
}
}
for (Card c : opposition) {
if (c.hasKeyword("First Strike") || c.hasKeyword("Double Strike")) {
oppHasFirstStrike = true;
}
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)) {
oppCantDie = false;
}
}
if (!isBlocking) {
int oppLife = combat.getDefendingPlayerRelatedTo(source).getLife();
if (((unblocked || canTrample) && (predictedPT.getLeft() - oppT > oppLife / 2))
|| (canTrample && predictedPT.getLeft() - oppT > 0 && predictedPT.getRight() > oppP)) {
// We can deal a lot of damage (either a lot of damage directly to the opponent,
// or kill the blocker(s) and damage the opponent at the same time, so go for it
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.MANDATORY_ATTACKERS);
return true;
}
}
if (predictedPT.getRight() - source.getDamage() <= oppP && oppHasFirstStrike && !cantDie) {
// Can't survive first strike or double strike, don't pump
return false;
}
if (predictedPT.getLeft() < oppT && (!cantDie || predictedPT.getRight() - source.getDamage() <= oppP)) {
// Can't pump enough to kill the blockers and survive, don't pump
return false;
}
if (source.getNetCombatDamage() > oppT && source.getNetToughness() > oppP) {
// Already enough to kill the blockers and survive, don't overpump
return false;
}
if (oppCantDie && !source.hasKeyword("Trample") && !source.hasKeyword("Wither")
&& !source.hasKeyword("Infect") && predictedPT.getLeft() <= oppT) {
// Can't kill or cripple anyone, as well as can't Trample over, so don't pump
return false;
}
// If we got here, it should be a favorable combat pump, resulting in at least one
// opposing creature dying, and hopefully with the Pummeler surviving combat.
return true;
}
public static boolean predictOverwhelmingDamage(final Player ai, final SpellAbility sa) {
final Card source = sa.getHostCard();
int oppLife = ai.getWeakestOpponent().getLife();
CardCollection oppInPlay = ai.getWeakestOpponent().getCreaturesInPlay();
CardCollection potentialBlockers = new CardCollection();
for (Card b : oppInPlay) {
if (CombatUtil.canBlock(sa.getHostCard(), b)) {
potentialBlockers.add(b);
}
}
Pair<Integer, Integer> predictedPT = getPumpedPT(ai, source.getNetCombatDamage(), source.getNetToughness());
int oppT = Aggregates.sum(potentialBlockers, CardPredicates.Accessors.fnGetNetToughness);
if (potentialBlockers.isEmpty() || (sa.getHostCard().hasKeyword("Trample") && predictedPT.getLeft() - oppT >= oppLife)) {
return true;
}
return false;
}
public static Pair<Integer, Integer> getPumpedPT(Player ai, int power, int toughness) {
int energy = ai.getCounters(CounterType.ENERGY);
if (energy > 0) {
int numActivations = energy / 3;
for (int i = 0; i < numActivations; i++) {
power *= 2;
toughness *= 2;
}
}
return Pair.of(power, toughness);
}
}
// Force of Will // Force of Will
public static class ForceOfWill { public static class ForceOfWill {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollection blueCards = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.isColor(MagicColor.BLUE)); CardCollection blueCards = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.isColor(MagicColor.BLUE));
boolean isExileMode = false; boolean isExileMode = false;
@@ -272,8 +495,9 @@ public class SpecialCardAi {
} }
} }
// Guilty Conscience
public static class GuiltyConscience { public static class GuiltyConscience {
public static Card getBestAttachTarget(final Player ai, SpellAbility sa, List<Card> list) { public static Card getBestAttachTarget(final Player ai, final SpellAbility sa, final List<Card> list) {
Card chosen = null; Card chosen = null;
List<Card> aiStuffies = CardLists.filter(list, new Predicate<Card>() { List<Card> aiStuffies = CardLists.filter(list, new Predicate<Card>() {
@@ -308,10 +532,148 @@ public class SpecialCardAi {
} }
} }
// Intuition (and any other card that might potentially let you pick N cards from the library,
// one of which will then be picked for you by the opponent)
public static class Intuition {
public static CardCollection considerMultiple(final Player ai, final SpellAbility sa) {
if (ai.getController().isAI()) {
if (!((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.INTUITION_ALTERNATIVE_LOGIC)) {
return new CardCollection(); // fall back to standard ChangeZoneAi considerations
}
}
int changeNum = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("ChangeNum"), sa);
CardCollection lib = CardLists.filter(ai.getCardsIn(ZoneType.Library),
Predicates.not(CardPredicates.nameEquals(sa.getHostCard().getName())));
Collections.sort(lib, CardLists.CmcComparatorInv);
// Additional cards which are difficult to auto-classify but which are generally good to Intuition for
List<String> highPriorityNamedCards = Lists.newArrayList("Accumulated Knowledge", "Take Inventory");
// figure out how many of each card we have in deck
MapToAmount<String> cardAmount = new LinkedHashMapToAmount<>();
for (Card c : lib) {
cardAmount.add(c.getName());
}
// Trix: see if we can complete the combo (if it looks like we might win shortly or if we need to get a Donate stat)
boolean donateComboMightWin = false;
int numIllusionsOTB = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals("Illusions of Grandeur")).size();
if (ai.getOpponentsSmallestLifeTotal() < 20 || numIllusionsOTB > 0) {
donateComboMightWin = true;
int numIllusionsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals("Illusions of Grandeur")).size();
int numDonateInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals("Donate")).size();
int numIllusionsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.nameEquals("Illusions of Grandeur")).size();
int numDonateInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.nameEquals("Donate")).size();
CardCollection comboList = new CardCollection();
if ((numIllusionsInHand > 0 || numIllusionsOTB > 0) && numDonateInHand == 0 && numDonateInLib >= 3) {
for (Card c : lib) {
if (c.getName().equals("Donate")) {
comboList.add(c);
}
}
return comboList;
} else if (numDonateInHand > 0 && numIllusionsInHand == 0 && numIllusionsInLib >= 3) {
for (Card c : lib) {
if (c.getName().equals("Illusions of Grandeur")) {
comboList.add(c);
}
}
return comboList;
}
}
// Create a priority list for cards that we have no more than 4 of and that are not lands
CardCollection libPriorityList = new CardCollection();
CardCollection libHighPriorityList = new CardCollection();
CardCollection libLowPriorityList = new CardCollection();
List<String> processed = Lists.newArrayList();
for (int i = 4; i > 0; i--) {
for (Card c : lib) {
if (!donateComboMightWin && (c.getName().equals("Illusions of Grandeur") || c.getName().equals("Donate"))) {
// Probably not worth putting two of the combo pieces into the graveyard
// since one Illusions-Donate is likely to not be enough
continue;
}
if (cardAmount.get(c.getName()) == i && !c.isLand() && !processed.contains(c.getName())) {
// if it's a card that is generally good to place in the graveyard, also add it
// to the mix
boolean canRetFromGrave = false;
String name = c.getName().replace(',', ';');
for (Trigger t : c.getTriggers()) {
SpellAbility ab = null;
if (t.hasParam("Execute")) {
ab = AbilityFactory.getAbility(c.getSVar(t.getParam("Execute")), c);
}
if (ab == null) { continue; }
if (ab.getApi() == ApiType.ChangeZone
&& "Self".equals(ab.getParam("Defined"))
&& "Graveyard".equals(ab.getParam("Origin"))
&& "Battlefield".equals(ab.getParam("Destination"))) {
canRetFromGrave = true;
}
if (ab.getApi() == ApiType.ChangeZoneAll
&& TextUtil.concatNoSpace("Creature.named", name).equals(ab.getParam("ChangeType"))
&& "Graveyard".equals(ab.getParam("Origin"))
&& "Battlefield".equals(ab.getParam("Destination"))) {
canRetFromGrave = true;
}
}
boolean isGoodToPutInGrave = c.hasSVar("DiscardMe") || canRetFromGrave
|| (ComputerUtil.isPlayingReanimator(ai) && c.isCreature());
for (Card c1 : lib) {
if (c1.getName().equals(c.getName())) {
if (CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(c1.getName())).isEmpty()
&& ComputerUtilMana.hasEnoughManaSourcesToCast(c1.getFirstSpellAbility(), ai)) {
// Try not to search for things we already have in hand or that we can't cast
libPriorityList.add(c1);
} else {
libLowPriorityList.add(c1);
}
if (isGoodToPutInGrave || highPriorityNamedCards.contains(c.getName())) {
libHighPriorityList.add(c1);
}
}
}
processed.add(c.getName());
}
}
}
// If we're playing Reanimator, we're really interested just in the highest CMC spells, not the
// ones we necessarily have multiples of
if (ComputerUtil.isPlayingReanimator(ai)) {
Collections.sort(libHighPriorityList, CardLists.CmcComparatorInv);
}
// Otherwise, try to grab something that is hopefully decent to grab, in priority order
CardCollection chosen = new CardCollection();
if (libHighPriorityList.size() >= changeNum) {
for (int i = 0; i < changeNum; i++) {
chosen.add(libHighPriorityList.get(i));
}
} else if (libPriorityList.size() >= changeNum) {
for (int i = 0; i < changeNum; i++) {
chosen.add(libPriorityList.get(i));
}
} else if (libLowPriorityList.size() >= changeNum) {
for (int i = 0; i < changeNum; i++) {
chosen.add(libLowPriorityList.get(i));
}
}
return chosen;
}
}
// Living Death (and possibly other similar cards using AILogic LivingDeath) // Living Death (and possibly other similar cards using AILogic LivingDeath)
public static class LivingDeath { public static class LivingDeath {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
int aiBattlefieldPower = 0, aiGraveyardPower = 0; int aiBattlefieldPower = 0, aiGraveyardPower = 0;
int threshold = 320; // approximately a 4/4 Flying creature worth of extra value
CardCollection aiCreaturesInGY = CardLists.filter(ai.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES); CardCollection aiCreaturesInGY = CardLists.filter(ai.getZone(ZoneType.Graveyard).getCards(), CardPredicates.Presets.CREATURES);
if (aiCreaturesInGY.isEmpty()) { if (aiCreaturesInGY.isEmpty()) {
@@ -348,13 +710,67 @@ public class SpecialCardAi {
} }
// if we get more value out of this than our opponent does (hopefully), go for it // if we get more value out of this than our opponent does (hopefully), go for it
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower); return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower + threshold);
}
}
// Mairsil, the Pretender
public static class MairsilThePretender {
// Scan the fetch list for a card with at least one activated ability.
// TODO: can be improved to a full consider(sa, ai) logic which would scan the graveyard first and hand last
public static Card considerCardFromList(final CardCollection fetchList) {
for (Card c : CardLists.filter(fetchList, Predicates.or(CardPredicates.Presets.ARTIFACTS, CardPredicates.Presets.CREATURES))) {
for (SpellAbility ab : c.getSpellAbilities()) {
if (ab.isAbility() && !ab.isTrigger()) {
Player controller = c.getController();
boolean wasCaged = false;
for (Card caged : CardLists.filter(controller.getCardsIn(ZoneType.Exile),
CardPredicates.hasCounter(CounterType.CAGE))) {
if (c.getName().equals(caged.getName())) {
wasCaged = true;
break;
}
}
if (!wasCaged) {
return c;
}
}
}
}
return null;
}
}
// Momir Vig, Simic Visionary Avatar
public static class MomirVigAvatar {
public static boolean consider(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) {
return false;
}
// Set PayX here to maximum value.
int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai);
// Some basic strategy for Momir
if (tokenSize < 2) {
return false;
}
if (tokenSize > 11) {
tokenSize = 11;
}
source.setSVar("PayX", Integer.toString(tokenSize));
return true;
} }
} }
// Necropotence // Necropotence
public static class Necropotence { public static class Necropotence {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
Game game = ai.getGame(); Game game = ai.getGame();
int computerHandSize = ai.getZone(ZoneType.Hand).size(); int computerHandSize = ai.getZone(ZoneType.Hand).size();
int maxHandSize = ai.getMaxHandSize(); int maxHandSize = ai.getMaxHandSize();
@@ -407,9 +823,34 @@ public class SpecialCardAi {
} }
} }
// Null Brooch
public static class NullBrooch {
public static boolean consider(final Player ai, final SpellAbility sa) {
// TODO: improve the detection of Ensnaring Bridge type effects ("GTX", "X" need generalization)
boolean hasEnsnaringBridgeEffect = false;
for (Card otb : ai.getCardsIn(ZoneType.Battlefield)) {
for (StaticAbility stab : otb.getStaticAbilities()) {
if ("CARDNAME can't attack.".equals(stab.getParam("AddHiddenKeyword"))
&& "Creature.powerGTX".equals(stab.getParam("Affected"))
&& "Count$InYourHand".equals(otb.getSVar("X"))) {
hasEnsnaringBridgeEffect = true;
break;
}
}
}
// Maybe use it for some important high-impact spells even if there are more cards in hand?
if (ai.getCardsIn(ZoneType.Hand).size() > 1 && !hasEnsnaringBridgeEffect) {
return false;
}
return true;
}
}
// Nykthos, Shrine to Nyx // Nykthos, Shrine to Nyx
public static class NykthosShrineToNyx { public static class NykthosShrineToNyx {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
Game game = ai.getGame(); Game game = ai.getGame();
PhaseHandler ph = game.getPhaseHandler(); PhaseHandler ph = game.getPhaseHandler();
if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) { if (!ph.isPlayerTurn(ai) || ph.getPhase().isBefore(PhaseType.MAIN2)) {
@@ -428,7 +869,7 @@ public class SpecialCardAi {
final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command}); final CardCollectionView cards = ai.getCardsIn(new ZoneType[] {ZoneType.Hand, ZoneType.Battlefield, ZoneType.Command});
List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai); List<SpellAbility> all = ComputerUtilAbility.getSpellAbilities(cards, ai);
int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableMana(ai, true), CardPredicates.Presets.UNTAPPED).size(); int numManaSrcs = CardLists.filter(ComputerUtilMana.getAvailableManaSources(ai, true), CardPredicates.Presets.UNTAPPED).size();
for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) { for (final SpellAbility testSa : ComputerUtilAbility.getOriginalAndAltCostAbilities(all, ai)) {
ManaCost cost = testSa.getPayCosts().getTotalMana(); ManaCost cost = testSa.getPayCosts().getTotalMana();
@@ -468,7 +909,7 @@ public class SpecialCardAi {
// Phyrexian Dreadnought // Phyrexian Dreadnought
public static class PhyrexianDreadnought { public static class PhyrexianDreadnought {
public static CardCollection reviseCreatureSacList(Player ai, SpellAbility sa, CardCollection choices) { public static CardCollection reviseCreatureSacList(final Player ai, final SpellAbility sa, final CardCollection choices) {
choices.sort(Collections.reverseOrder(ComputerUtilCard.EvaluateCreatureComparator)); choices.sort(Collections.reverseOrder(ComputerUtilCard.EvaluateCreatureComparator));
int power = 0; int power = 0;
List<Card> toKeep = Lists.newArrayList(); List<Card> toKeep = Lists.newArrayList();
@@ -492,11 +933,11 @@ public class SpecialCardAi {
// Sarkhan the Mad // Sarkhan the Mad
public static class SarkhanTheMad { public static class SarkhanTheMad {
public static boolean considerDig(Player ai, SpellAbility sa) { public static boolean considerDig(final Player ai, final SpellAbility sa) {
return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1; return sa.getHostCard().getCounters(CounterType.LOYALTY) == 1;
} }
public static boolean considerMakeDragon(Player ai, SpellAbility sa) { public static boolean considerMakeDragon(final Player ai, final SpellAbility sa) {
// TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier? // TODO: expand this logic to make the AI force the opponent to sacrifice a big threat bigger than a 5/5 flier?
CardCollection creatures = ai.getCreaturesInPlay(); CardCollection creatures = ai.getCreaturesInPlay();
boolean hasValidTgt = !CardLists.filter(creatures, new Predicate<Card>() { boolean hasValidTgt = !CardLists.filter(creatures, new Predicate<Card>() {
@@ -513,7 +954,7 @@ public class SpecialCardAi {
return false; return false;
} }
public static boolean considerUltimate(Player ai, SpellAbility sa, Player weakestOpp) { public static boolean considerUltimate(final Player ai, final SpellAbility sa, final Player weakestOpp) {
int minLife = weakestOpp.getLife(); int minLife = weakestOpp.getLife();
int dragonPower = 0; int dragonPower = 0;
@@ -526,9 +967,123 @@ public class SpecialCardAi {
} }
} }
// Survival of the Fittest
public static class SurvivalOfTheFittest {
public static Card considerDiscardTarget(final Player ai) {
// The AI here only checks the number of available creatures of various CMC, which is equivalent to knowing
// your deck composition and checking (and counting) the cards in other zones so you know what you have left
// in the library. As such, this does not cause unfair advantage, at least unless there are cards that are
// face down (on the battlefield or in exile). Might need some kind of an update to consider hidden information
// like that properly (probably by adding all those cards to the evaluation mix so the AI doesn't "know" which
// ones are already face down in play and which are still in the library)
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
CardCollectionView creatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES);
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
if (creatsInHand.isEmpty() || creatsInLib.isEmpty()) { return null; }
int numManaSrcs = ComputerUtilMana.getAvailableManaEstimate(ai, false)
+ Math.min(1, manaSrcsInHand.size());
// Cards in library that are either below/at (preferred) or above the max CMC affordable by the AI
// (the latter might happen if we're playing a Reanimator deck with lots of fatties)
CardCollection atTargetCMCInLib = CardLists.filter(creatsInLib, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilMana.hasEnoughManaSourcesToCast(card.getSpellPermanent(), ai);
}
});
if (atTargetCMCInLib.isEmpty()) {
atTargetCMCInLib = CardLists.filter(creatsInLib, CardPredicates.greaterCMC(numManaSrcs));
}
Collections.sort(atTargetCMCInLib, CardLists.CmcComparatorInv);
if (atTargetCMCInLib.isEmpty()) {
// Nothing to aim for?
return null;
}
// Cards in hand that are below the max CMC affordable by the AI
CardCollection belowMaxCMC = CardLists.filter(creatsInHand, CardPredicates.lessCMC(numManaSrcs - 1));
Collections.sort(belowMaxCMC, Collections.reverseOrder(CardLists.CmcComparatorInv));
// Cards in hand that are above the max CMC affordable by the AI
CardCollection aboveMaxCMC = CardLists.filter(creatsInHand, CardPredicates.greaterCMC(numManaSrcs + 1));
Collections.sort(aboveMaxCMC, CardLists.CmcComparatorInv);
Card maxCMC = !aboveMaxCMC.isEmpty() ? aboveMaxCMC.getFirst() : null;
Card minCMC = !belowMaxCMC.isEmpty() ? belowMaxCMC.getFirst() : null;
Card bestInLib = !atTargetCMCInLib.isEmpty() ? atTargetCMCInLib.getFirst() : null;
int maxCMCdiff = 0;
if (maxCMC != null) {
maxCMCdiff = maxCMC.getCMC() - numManaSrcs; // how far are we from viably casting it?
}
// We have something too fat to viably cast in the nearest future, discard it hoping to
// grab something more immediately valuable (or maybe we're playing Reanimator and we want
// it to be in the graveyard anyway)
if (maxCMCdiff >= 3) {
return maxCMC;
}
// We have a card in hand that is worse than the one in library, so discard the worst card
if (maxCMCdiff <= 0 && minCMC != null
&& ComputerUtilCard.evaluateCreature(bestInLib) > ComputerUtilCard.evaluateCreature(minCMC)) {
return minCMC;
}
// We have a card in the library that is closer to being castable than the one in hand, and
// no options with smaller CMC, so discard the one that is harder to cast for the one that is
// easier to cast right now, but only if the best card in the library is at least CMC 3
// (probably not worth it to grab low mana cost cards this way)
if (maxCMC != null && maxCMC.getCMC() < bestInLib.getCMC() && bestInLib.getCMC() >= 3) {
return maxCMC;
}
// We appear to be playing Reanimator (or we have a reanimator card in hand already), so it's
// worth to fill the graveyard now
if (ComputerUtil.isPlayingReanimator(ai) && !creatsInLib.isEmpty()) {
CardCollection creatsInHandByCMC = new CardCollection(creatsInHand);
Collections.sort(creatsInHandByCMC, CardLists.CmcComparatorInv);
return creatsInHandByCMC.getFirst();
}
// probably nothing that is worth changing, so bail
return null;
}
public static Card considerCardToGet(final Player ai, final SpellAbility sa) {
CardCollectionView creatsInLib = CardLists.filter(ai.getCardsIn(ZoneType.Library), CardPredicates.Presets.CREATURES);
if (creatsInLib.isEmpty()) { return null; }
CardCollectionView manaSrcsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA);
int numManaSrcs = ComputerUtilMana.getAvailableManaEstimate(ai, false)
+ Math.min(1, manaSrcsInHand.size());
CardCollection atTargetCMCInLib = CardLists.filter(creatsInLib, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilMana.hasEnoughManaSourcesToCast(card.getSpellPermanent(), ai);
}
});
if (atTargetCMCInLib.isEmpty()) {
atTargetCMCInLib = CardLists.filter(creatsInLib, CardPredicates.greaterCMC(numManaSrcs));
}
Collections.sort(atTargetCMCInLib, CardLists.CmcComparatorInv);
Card bestInLib = atTargetCMCInLib != null ? atTargetCMCInLib.getFirst() : null;
if (bestInLib == null && ComputerUtil.isPlayingReanimator(ai)) {
// For Reanimator, we don't mind grabbing the biggest thing possible to recycle it again with SotF later.
CardCollection creatsInLibByCMC = new CardCollection(creatsInLib);
Collections.sort(creatsInLibByCMC, CardLists.CmcComparatorInv);
return creatsInLibByCMC.getFirst();
}
return bestInLib;
}
}
// Timetwister // Timetwister
public static class Timetwister { public static class Timetwister {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size(); final int aiHandSize = ai.getCardsIn(ZoneType.Hand).size();
int maxOppHandSize = 0; int maxOppHandSize = 0;
@@ -550,8 +1105,44 @@ public class SpecialCardAi {
} }
} }
// Volrath's Shapeshifter
public static class VolrathsShapeshifter {
public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollectionView aiGY = ai.getCardsIn(ZoneType.Graveyard);
Card topGY = null;
Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand));
int numCreatsInHand = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.CREATURES).size();
if (!aiGY.isEmpty()) {
topGY = ai.getCardsIn(ZoneType.Graveyard).get(0);
}
if ((topGY != null && !topGY.isCreature()) || creatHand != null) {
if (numCreatsInHand > 1 || !ComputerUtilMana.canPayManaCost(creatHand.getSpellPermanent(), ai, 0)) {
return true;
}
}
return false;
}
public static CardCollection targetBestCreature(final Player ai, final SpellAbility sa) {
Card creatHand = ComputerUtilCard.getBestCreatureAI(ai.getCardsIn(ZoneType.Hand));
if (creatHand != null) {
CardCollection cc = new CardCollection();
cc.add(creatHand);
return cc;
}
// Should ideally never get here
System.err.println("Volrath's Shapeshifter AI: Could not find a discard target despite the previous confirmation to proceed!");
return null;
}
}
// Ugin, the Spirit Dragon
public static class UginTheSpiritDragon { public static class UginTheSpiritDragon {
public static boolean considerPWAbilityPriority(Player ai, SpellAbility sa, ZoneType origin, CardCollectionView oppType, CardCollectionView computerType) { public static boolean considerPWAbilityPriority(final Player ai, final SpellAbility sa, final ZoneType origin, CardCollectionView oppType, CardCollectionView computerType) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
Game game = source.getGame(); Game game = source.getGame();
@@ -610,7 +1201,7 @@ public class SpecialCardAi {
// Yawgmoth's Bargain // Yawgmoth's Bargain
public static class YawgmothsBargain { public static class YawgmothsBargain {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
Game game = ai.getGame(); Game game = ai.getGame();
PhaseHandler ph = game.getPhaseHandler(); PhaseHandler ph = game.getPhaseHandler();
@@ -653,7 +1244,7 @@ public class SpecialCardAi {
// Yawgmoth's Will (can potentially be expanded for other broadly similar effects too) // Yawgmoth's Will (can potentially be expanded for other broadly similar effects too)
public static class YawgmothsWill { public static class YawgmothsWill {
public static boolean consider(Player ai, SpellAbility sa) { public static boolean consider(final Player ai, final SpellAbility sa) {
CardCollectionView cardsInGY = ai.getCardsIn(ZoneType.Graveyard); CardCollectionView cardsInGY = ai.getCardsIn(ZoneType.Graveyard);
if (cardsInGY.size() == 0) { if (cardsInGY.size() == 0) {
return false; return false;

View File

@@ -1,12 +1,7 @@
package forge.ai; package forge.ai;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.card.ICardFace; import forge.card.ICardFace;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostParser; import forge.card.mana.ManaCostParser;
@@ -25,6 +20,10 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.SpellAbilityCondition;
import forge.util.MyRandom; import forge.util.MyRandom;
import java.util.Collection;
import java.util.List;
import java.util.Map;
/** /**
* Base class for API-specific AI logic * Base class for API-specific AI logic
* <p> * <p>
@@ -103,6 +102,12 @@ public abstract class SpellAbilityAi {
* Checks if the AI will play a SpellAbility with the specified AiLogic * Checks if the AI will play a SpellAbility with the specified AiLogic
*/ */
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (aiLogic.equals("CheckCondition")) {
SpellAbility saCopy = sa.copy();
saCopy.setActivatingPlayer(ai);
return saCopy.getConditions().areMet(saCopy);
}
return !("Never".equals(aiLogic)); return !("Never".equals(aiLogic));
} }
@@ -118,7 +123,7 @@ public abstract class SpellAbilityAi {
if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) { if (!ComputerUtilCost.checkDiscardCost(ai, cost, source)) {
return false; return false;
} }
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa)) {
return false; return false;
} }
if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) { if (!ComputerUtilCost.checkRemoveCounterCost(cost, source, sa)) {

View File

@@ -1,14 +1,13 @@
package forge.ai; package forge.ai;
import java.util.Map;
import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.ability.*; import forge.ai.ability.*;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.util.ReflectionUtil; import forge.util.ReflectionUtil;
import java.util.Map;
public enum SpellApiToAi { public enum SpellApiToAi {
Converter; Converter;
@@ -31,6 +30,7 @@ public enum SpellApiToAi {
.put(ApiType.BidLife, BidLifeAi.class) .put(ApiType.BidLife, BidLifeAi.class)
.put(ApiType.Bond, BondAi.class) .put(ApiType.Bond, BondAi.class)
.put(ApiType.Branch, AlwaysPlayAi.class) .put(ApiType.Branch, AlwaysPlayAi.class)
.put(ApiType.ChangeCombatants, CannotPlayAi.class)
.put(ApiType.ChangeTargets, ChangeTargetsAi.class) .put(ApiType.ChangeTargets, ChangeTargetsAi.class)
.put(ApiType.ChangeZone, ChangeZoneAi.class) .put(ApiType.ChangeZone, ChangeZoneAi.class)
.put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class) .put(ApiType.ChangeZoneAll, ChangeZoneAllAi.class)
@@ -71,12 +71,14 @@ public enum SpellApiToAi {
.put(ApiType.ExchangeControlVariant, CannotPlayAi.class) .put(ApiType.ExchangeControlVariant, CannotPlayAi.class)
.put(ApiType.ExchangePower, PowerExchangeAi.class) .put(ApiType.ExchangePower, PowerExchangeAi.class)
.put(ApiType.ExchangeZone, ZoneExchangeAi.class) .put(ApiType.ExchangeZone, ZoneExchangeAi.class)
.put(ApiType.Explore, ExploreAi.class)
.put(ApiType.Fight, FightAi.class) .put(ApiType.Fight, FightAi.class)
.put(ApiType.FlipACoin, FlipACoinAi.class) .put(ApiType.FlipACoin, FlipACoinAi.class)
.put(ApiType.Fog, FogAi.class) .put(ApiType.Fog, FogAi.class)
.put(ApiType.GainControl, ControlGainAi.class) .put(ApiType.GainControl, ControlGainAi.class)
.put(ApiType.GainLife, LifeGainAi.class) .put(ApiType.GainLife, LifeGainAi.class)
.put(ApiType.GainOwnership, CannotPlayAi.class) .put(ApiType.GainOwnership, CannotPlayAi.class)
.put(ApiType.GameDrawn, CannotPlayAi.class)
.put(ApiType.GenericChoice, ChooseGenericEffectAi.class) .put(ApiType.GenericChoice, ChooseGenericEffectAi.class)
.put(ApiType.Goad, GoadAi.class) .put(ApiType.Goad, GoadAi.class)
.put(ApiType.Haunt, HauntAi.class) .put(ApiType.Haunt, HauntAi.class)

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -21,7 +22,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final Random r = MyRandom.getRandom(); final Random r = MyRandom.getRandom();
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
@@ -46,7 +47,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -87,7 +88,7 @@ public class ActivateAbilityAi extends SpellAbilityAi {
} }
} else { } else {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(ai.getOpponent()); sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} }
return randomReturn; return randomReturn;

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
public class AlwaysPlayAi extends SpellAbilityAi { public class AlwaysPlayAi extends SpellAbilityAi {
@@ -13,4 +14,9 @@ public class AlwaysPlayAi extends SpellAbilityAi {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true; return true;
} }
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
} }

View File

@@ -1,28 +1,15 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.AiCardMemory; import forge.ai.*;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
import forge.card.CardType; import forge.card.CardType;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardUtil;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -38,6 +25,10 @@ import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.collect.FCollectionView; import forge.util.collect.FCollectionView;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
/** /**
* <p> * <p>
@@ -86,13 +77,13 @@ public class AnimateAi extends SpellAbilityAi {
num = (num == null) ? "1" : num; num = (num == null) ? "1" : num;
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack); final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
ai.getOpponent(), topStack.getHostCard(), topStack); ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
ComputerUtilCard.sortByEvaluateCreature(list); ComputerUtilCard.sortByEvaluateCreature(list);
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) { if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
Card animatedCopy = becomeAnimated(source, sa); Card animatedCopy = becomeAnimated(source, sa);
list.add(animatedCopy); list.add(animatedCopy);
list = CardLists.getValidCards(list, valid.split(","), ai.getOpponent(), topStack.getHostCard(), list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(),
topStack); topStack);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0)) if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))
@@ -172,7 +163,9 @@ public class AnimateAi extends SpellAbilityAi {
} }
} }
if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) { if (power + toughness > c.getCurrentPower() + c.getCurrentToughness()) {
bFlag = true; if (!c.isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(c))) {
bFlag = true;
}
} }
} }
@@ -186,9 +179,21 @@ public class AnimateAi extends SpellAbilityAi {
&& !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) { && !ComputerUtilCard.doesSpecifiedCreatureBlock(aiPlayer, animatedCopy)) {
return false; return false;
} }
this.rememberAnimatedThisTurn(aiPlayer, c); // also check if maybe there are static effects applied to the animated copy that would matter
// (e.g. Myth Realized)
if (animatedCopy.getCurrentPower() + animatedCopy.getCurrentToughness() >
c.getCurrentPower() + c.getCurrentToughness()) {
if (!isAnimatedThisTurn(aiPlayer, sa.getHostCard())) {
if (!sa.getHostCard().isTapped() || (game.getCombat() != null && game.getCombat().isAttacking(sa.getHostCard()))) {
bFlag = true;
}
}
}
} }
} }
if (bFlag) {
this.rememberAnimatedThisTurn(aiPlayer, sa.getHostCard());
}
return bFlag; // All of the defined stuff is animated, not very useful return bFlag; // All of the defined stuff is animated, not very useful
} else { } else {
sa.resetTargets(); sa.resetTargets();

View File

@@ -38,7 +38,7 @@ public class AttachAi extends SpellAbilityAi {
if (abCost != null) { if (abCost != null) {
// AI currently disabled for these costs // AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false; return false;
} }
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
@@ -250,6 +250,18 @@ public class AttachAi extends SpellAbilityAi {
return false; return false;
} }
if (!mandatory) {
if (!c.isCreature() && !c.getType().hasSubtype("Vehicle") && !c.isTapped()) {
// try to identify if this thing can actually tap
for (SpellAbility ab : c.getAllSpellAbilities()) {
if (ab.getPayCosts() != null && ab.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
}
}
if (!c.isEnchanted()) { if (!c.isEnchanted()) {
return true; return true;
} }
@@ -580,8 +592,8 @@ public class AttachAi extends SpellAbilityAi {
continue; continue;
} }
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) { if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness")); totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower")); totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
String kws = stabMap.get("AddKeyword"); String kws = stabMap.get("AddKeyword");
if (kws != null) { if (kws != null) {
@@ -702,30 +714,6 @@ public class AttachAi extends SpellAbilityAi {
return true; return true;
} }
/**
* parseSVar TODO - flesh out javadoc for this method.
*
* @param hostCard
* the Card with the SVar on it
* @param amount
* a String
* @return the calculated number
*/
public static int parseSVar(final Card hostCard, final String amount) {
int num = 0;
if (amount == null) {
return num;
}
try {
num = Integer.valueOf(amount);
} catch (final NumberFormatException e) {
num = CardFactoryUtil.xCount(hostCard, hostCard.getSVar(amount).split("\\$")[1]);
}
return num;
}
/** /**
* Attach preference. * Attach preference.
* *
@@ -874,8 +862,8 @@ public class AttachAi extends SpellAbilityAi {
continue; continue;
} }
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) { if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness")); totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower")); totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
grantingAbilities |= stabMap.containsKey("AddAbility"); grantingAbilities |= stabMap.containsKey("AddAbility");
@@ -1077,10 +1065,10 @@ public class AttachAi extends SpellAbilityAi {
// make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around // make sure to prioritize casting spells in main 2 (creatures, other equipment, etc.) rather than moving equipment around
boolean decideMoveFromUseless = uselessCreature && aic.getBooleanProperty(AiProps.PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS); boolean decideMoveFromUseless = uselessCreature && aic.getBooleanProperty(AiProps.PRIORITIZE_MOVE_EQUIPMENT_IF_USELESS);
if (!decideMoveFromUseless && AiCardMemory.isMemorySetEmpty(aiPlayer, AiCardMemory.MemorySet.HELD_MANA_SOURCES)) { if (!decideMoveFromUseless && AiCardMemory.isMemorySetEmpty(aiPlayer, AiCardMemory.MemorySet.HELD_MANA_SOURCES_FOR_MAIN2)) {
SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Attach); SpellAbility futureSpell = aic.predictSpellToCastInMain2(ApiType.Attach);
if (futureSpell != null && futureSpell.getHostCard() != null) { if (futureSpell != null && futureSpell.getHostCard() != null) {
aic.reserveManaSourcesForMain2(futureSpell); aic.reserveManaSources(futureSpell);
} }
} }
@@ -1287,9 +1275,9 @@ public class AttachAi extends SpellAbilityAi {
if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) { if (card.hasKeyword("Flying") || !CombatUtil.canBlock(card, true)) {
return false; return false;
} }
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) { } else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
if (!CombatUtil.canBlock(card, true) || card.hasKeyword("CARDNAME can block any number of creatures.") if (!CombatUtil.canBlock(card, true) || card.hasKeyword("CARDNAME can block any number of creatures.")
|| card.hasKeyword("CARDNAME can block an additional ninety-nine creatures.")) { || card.hasKeyword("CARDNAME can block an additional ninety-nine creatures each combat.")) {
return false; return false;
} }
} else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) { } else if (keyword.equals("CARDNAME can attack as though it didn't have defender.")) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.card.CardCollectionView; import forge.game.card.CardCollectionView;
import forge.game.card.CardLists; import forge.game.card.CardLists;
@@ -16,7 +17,7 @@ public class BalanceAi extends SpellAbilityAi {
int diff = 0; int diff = 0;
// TODO Add support for multiplayer logic // TODO Add support for multiplayer logic
final Player opp = aiPlayer.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield); final CardCollectionView humPerms = opp.getCardsIn(ZoneType.Battlefield);
final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield); final CardCollectionView compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
@@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canTgtCreature()) { if (tgt.canTgtCreature()) {
List<Card> list = CardLists.getTargetableCards(aiPlayer.getOpponent().getCardsIn(ZoneType.Battlefield), sa); List<Card> list = CardLists.getTargetableCards(ComputerUtil.getOpponentFor(aiPlayer).getCardsIn(ZoneType.Battlefield), sa);
list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa); list = CardLists.getValidCards(list, tgt.getValidTgts(), source.getController(), source, sa);
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;

View File

@@ -36,10 +36,10 @@ public final class BondAi extends SpellAbilityAi {
* <p> * <p>
* bondCanPlayAI. * bondCanPlayAI.
* </p> * </p>
* @param aiPlayer
* a {@link forge.game.player.Player} object.
* @param sa * @param sa
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @param af
* a {@link forge.game.ability.AbilityFactory} object.
* *
* @return a boolean. * @return a boolean.
*/ */
@@ -53,4 +53,9 @@ public final class BondAi extends SpellAbilityAi {
protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) { protected Card chooseSingleCard(Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
return ComputerUtilCard.getBestCreatureAI(options); return ComputerUtilCard.getBestCreatureAI(options);
} }
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
return true;
}
} }

View File

@@ -1,20 +1,10 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates; import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.*; import forge.ai.*;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.game.Game; import forge.game.Game;
@@ -22,11 +12,7 @@ import forge.game.GameObject;
import forge.game.GlobalRuleChange; import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -40,6 +26,9 @@ import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
public class ChangeZoneAi extends SpellAbilityAi { public class ChangeZoneAi extends SpellAbilityAi {
/* /*
@@ -49,6 +38,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
* too much: blink/bounce/exile/tutor/Raise Dead/Surgical Extraction/...... * too much: blink/bounce/exile/tutor/Raise Dead/Surgical Extraction/......
*/ */
// multipleCardsToChoose is used by Intuition and can be adapted to be used by other
// cards where multiple cards are fetched at once and they need to be coordinated
private static CardCollection multipleCardsToChoose = new CardCollection();
@Override @Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) { if (sa.getHostCard() != null && sa.getHostCard().hasSVar("AIPreferenceOverride")) {
@@ -64,23 +57,45 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) { if (ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false; return false;
} }
} else if (aiLogic.equals("PriorityOptionalCost")) {
boolean highPriority = false;
// if we have more than one of these in hand, might not be worth waiting for optional cost payment on the additional copy
highPriority |= CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.nameEquals(sa.getHostCard().getName())).size() > 1;
// if we are in danger in combat, no need to wait to pay the optional cost
highPriority |= ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& ai.getGame().getCombat() != null && ComputerUtilCombat.lifeInDanger(ai, ai.getGame().getCombat());
if (!highPriority) {
if (Iterables.isEmpty(sa.getOptionalCosts())) {
return false;
}
}
} }
return super.checkAiLogic(ai, sa, aiLogic); return super.checkAiLogic(ai, sa, aiLogic);
} }
@Override @Override
protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) { protected boolean checkApiLogic(Player aiPlayer, SpellAbility sa) {
// Checks for "return true" unlike checkAiLogic() // Checks for "return true" unlike checkAiLogic()
multipleCardsToChoose.clear();
String aiLogic = sa.getParam("AILogic"); String aiLogic = sa.getParam("AILogic");
if (aiLogic != null) { if (aiLogic != null) {
if (aiLogic.equals("Always")) { if (aiLogic.equals("Always")) {
return true; return true;
} else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc. } else if (aiLogic.startsWith("SacAndUpgrade")) { // Birthing Pod, Natural Order, etc.
return this.doSacAndUpgradeLogic(aiPlayer, sa); return this.doSacAndUpgradeLogic(aiPlayer, sa);
} else if (aiLogic.startsWith("SacAndRetFromGrave")) { // Recurring Nightmare, etc.
return this.doSacAndReturnFromGraveLogic(aiPlayer, sa);
} else if (aiLogic.equals("Necropotence")) { } else if (aiLogic.equals("Necropotence")) {
return SpecialCardAi.Necropotence.consider(aiPlayer, sa); return SpecialCardAi.Necropotence.consider(aiPlayer, sa);
} else if (aiLogic.equals("SameName")) { // Declaration in Stone } else if (aiLogic.equals("SameName")) { // Declaration in Stone
return this.doSameNameLogic(aiPlayer, sa); return this.doSameNameLogic(aiPlayer, sa);
} else if (aiLogic.equals("Intuition")) {
// This logic only fills the multiple cards array, the decision to play is made
// separately in hiddenOriginCanPlayAI later.
multipleCardsToChoose = SpecialCardAi.Intuition.considerMultiple(aiPlayer, sa);
} }
} }
if (isHidden(sa)) { if (isHidden(sa)) {
@@ -166,7 +181,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
ZoneType origin = null; ZoneType origin = null;
final Player opponent = ai.getOpponent(); final Player opponent = ComputerUtil.getOpponentFor(ai);
boolean activateForCost = ComputerUtil.activateForCost(sa, ai); boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) { if (sa.hasParam("Origin")) {
@@ -182,7 +197,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
if (abCost != null) { if (abCost != null) {
// AI currently disabled for these costs // AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source) if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)
&& !(destination.equals("Battlefield") && !source.isLand())) { && !(destination.equals("Battlefield") && !source.isLand())) {
return false; return false;
} }
@@ -367,7 +382,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// if putting cards from hand to library and parent is drawing cards // if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something: // make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = aiPlayer.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
if (tgt != null && tgt.canTgtPlayer()) { if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse(); boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opp)) { if (isCurse && sa.canTarget(opp)) {
@@ -428,7 +443,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
Iterable<Player> pDefined; Iterable<Player> pDefined;
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if ((tgt != null) && tgt.canTgtPlayer()) { if ((tgt != null) && tgt.canTgtPlayer()) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.isCurse()) { if (sa.isCurse()) {
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
@@ -547,8 +562,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/ */
private static Card chooseCreature(final Player ai, CardCollection list) { private static Card chooseCreature(final Player ai, CardCollection list) {
// Creating a new combat for testing purposes. // Creating a new combat for testing purposes.
Combat combat = new Combat(ai.getOpponent()); final Player opponent = ComputerUtil.getOpponentFor(ai);
for (Card att : ai.getOpponent().getCreaturesInPlay()) { Combat combat = new Combat(opponent);
for (Card att : opponent.getCreaturesInPlay()) {
combat.addAttacker(att, ai); combat.addAttacker(att, ai);
} }
AiBlockController block = new AiBlockController(ai); AiBlockController block = new AiBlockController(ai);
@@ -647,6 +663,20 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
} }
if (destination == ZoneType.Battlefield) {
// predict whether something may put a ETBing creature below zero toughness
// (e.g. Reassembing Skeleton + Elesh Norn, Grand Cenobite)
for (final Card c : retrieval) {
if (c.isCreature()) {
final Card copy = CardUtil.getLKICopy(c);
ComputerUtilCard.applyStaticContPT(c.getGame(), copy, null);
if (copy.getNetToughness() <= 0) {
return false;
}
}
}
}
} }
final AbilitySub subAb = sa.getSubAbility(); final AbilitySub subAb = sa.getSubAbility();
@@ -666,6 +696,12 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) { protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
if (aiLogic.equals("SurvivalOfTheFittest")) {
return ph.getNextTurn().equals(ai) && ph.is(PhaseType.END_OF_TURN);
}
if (isHidden(sa)) { if (isHidden(sa)) {
return true; return true;
} }
@@ -828,7 +864,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
&& !currCombat.getBlockers(attacker).isEmpty()) { && !currCombat.getBlockers(attacker).isEmpty()) {
ComputerUtilCard.sortByEvaluateCreature(blockers); ComputerUtilCard.sortByEvaluateCreature(blockers);
Combat combat = new Combat(ai); Combat combat = new Combat(ai);
combat.addAttacker(attacker, ai.getOpponent()); combat.addAttacker(attacker, ComputerUtil.getOpponentFor(ai));
for (Card blocker : blockers) { for (Card blocker : blockers) {
combat.addBlocker(attacker, blocker); combat.addBlocker(attacker, blocker);
} }
@@ -974,18 +1010,21 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
list = CardLists.filterControlledBy(list, ai.getOpponents());
list = CardLists.filter(list, new Predicate<Card>() { if (!sa.hasParam("AITgtOwnCards")) {
@Override list = CardLists.filterControlledBy(list, ai.getOpponents());
public boolean apply(final Card c) { list = CardLists.filter(list, new Predicate<Card>() {
for (Card aura : c.getEnchantedBy(false)) { @Override
if (c.getOwner().isOpponentOf(ai) && aura.getController().equals(ai)) { public boolean apply(final Card c) {
return false; for (Card aura : c.getEnchantedBy(false)) {
if (c.getOwner().isOpponentOf(ai) && aura.getController().equals(ai)) {
return false;
}
} }
return true;
} }
return true; });
} }
});
} }
// Only care about combatants during combat // Only care about combatants during combat
@@ -1000,6 +1039,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} }
// Check if the opponent can save a creature from bounce/blink/whatever by paying
// the Unless cost (for example, Erratic Portal)
list.removeAll(getSafeTargetsIfUnlessCostPaid(ai, sa, list));
if (!mandatory && list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) { if (!mandatory && list.size() < tgt.getMinTargets(sa.getHostCard(), sa)) {
return false; return false;
} }
@@ -1077,7 +1120,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
return false; return false;
} else { } else {
if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) { if (!sa.isTrigger() && !ComputerUtil.shouldCastLessThanMax(ai, source)) {
return false; boolean aiTgtsOK = false;
if (sa.hasParam("AIMinTgts")) {
int minTgts = Integer.parseInt(sa.getParam("AIMinTgts"));
if (sa.getTargets().getNumTargeted() >= minTgts) {
aiTgtsOK = true;
}
}
if (!aiTgtsOK) {
return false;
}
} }
break; break;
} }
@@ -1284,6 +1336,10 @@ public class ChangeZoneAi extends SpellAbilityAi {
* @return a boolean. * @return a boolean.
*/ */
private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) { private static boolean knownOriginTriggerAI(final Player ai, final SpellAbility sa, final boolean mandatory) {
if ("DeathgorgeScavenger".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.DeathgorgeScavenger.consider(ai, sa);
}
if (sa.getTargetRestrictions() == null) { if (sa.getTargetRestrictions() == null) {
// Just in case of Defined cases // Just in case of Defined cases
if (!mandatory && sa.hasParam("AttachedTo")) { if (!mandatory && sa.hasParam("AttachedTo")) {
@@ -1319,6 +1375,16 @@ public class ChangeZoneAi extends SpellAbilityAi {
} }
} else if ("WorstCard".equals(logic)) { } else if ("WorstCard".equals(logic)) {
return ComputerUtilCard.getWorstAI(fetchList); return ComputerUtilCard.getWorstAI(fetchList);
} else if ("Mairsil".equals(logic)) {
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
} else if ("SurvivalOfTheFittest".equals(logic)) {
return SpecialCardAi.SurvivalOfTheFittest.considerCardToGet(decider, sa);
} else if ("Intuition".equals(logic)) {
if (!multipleCardsToChoose.isEmpty()) {
Card choice = multipleCardsToChoose.get(0);
multipleCardsToChoose.remove(0);
return choice;
}
} }
} }
if (fetchList.isEmpty()) { if (fetchList.isEmpty()) {
@@ -1459,7 +1525,33 @@ public class ChangeZoneAi extends SpellAbilityAi {
return Iterables.getFirst(options, null); return Iterables.getFirst(options, null);
} }
private boolean doSacAndUpgradeLogic(final Player ai, SpellAbility sa) { private boolean doSacAndReturnFromGraveLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard();
String definedSac = StringUtils.split(source.getSVar("AIPreference"), "$")[1];
CardCollection listToSac = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.restriction(definedSac.split(","), ai, source, sa));
listToSac.sort(Collections.reverseOrder(CardLists.CmcComparatorInv));
CardCollection listToRet = CardLists.filter(ai.getCardsIn(ZoneType.Graveyard), Presets.CREATURES);
listToRet.sort(CardLists.CmcComparatorInv);
if (!listToSac.isEmpty() && !listToRet.isEmpty()) {
Card worstSac = listToSac.getFirst();
Card bestRet = listToRet.getFirst();
if (bestRet.getCMC() > worstSac.getCMC()
&& ComputerUtilCard.evaluateCreature(bestRet) > ComputerUtilCard.evaluateCreature(worstSac)) {
sa.resetTargets();
sa.getTargets().add(bestRet);
source.setSVar("AIPreferenceOverride", "Creature.cmcEQ" + worstSac.getCMC());
return true;
}
}
return false;
}
private boolean doSacAndUpgradeLogic(final Player ai, final SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
PhaseHandler ph = ai.getGame().getPhaseHandler(); PhaseHandler ph = ai.getGame().getPhaseHandler();
String logic = sa.getParam("AILogic"); String logic = sa.getParam("AILogic");
@@ -1623,6 +1715,45 @@ public class ChangeZoneAi extends SpellAbilityAi {
return true; return true;
} }
private static CardCollection getSafeTargetsIfUnlessCostPaid(Player ai, SpellAbility sa, Iterable<Card> potentialTgts) {
// Determines if the controller of each potential target can negate the ChangeZone effect
// by paying the Unless cost. Returns the list of targets that can be saved that way.
final Card source = sa.getHostCard();
final CardCollection canBeSaved = new CardCollection();
for (Card potentialTgt : potentialTgts) {
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
if (unlessCost != null && !unlessCost.endsWith(">")) {
Player opp = potentialTgt.getController();
int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
setPayX = true;
toPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
} else {
toPay = AbilityUtils.calculateAmount(source, unlessCost, sa);
}
if (toPay == 0) {
canBeSaved.add(potentialTgt);
}
if (toPay <= usableManaSources) {
canBeSaved.add(potentialTgt);
}
if (setPayX) {
source.setSVar("PayX", Integer.toString(toPay));
}
}
}
return canBeSaved;
}
private static void rememberBouncedThisTurn(Player ai, Card c) { private static void rememberBouncedThisTurn(Player ai, Card c) {
AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN); AiCardMemory.rememberCard(ai, c, AiCardMemory.MemorySet.BOUNCED_THIS_TURN);
} }

View File

@@ -1,24 +1,11 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Collections;
import java.util.Random;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.AiPlayerPredicates;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -28,6 +15,9 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import java.util.Collections;
import java.util.Random;
public class ChangeZoneAllAi extends SpellAbilityAi { public class ChangeZoneAllAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
@@ -73,14 +63,25 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa); oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa); computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
// Living Death AI
if ("LivingDeath".equals(sa.getParam("AILogic"))) { if ("LivingDeath".equals(sa.getParam("AILogic"))) {
// Living Death AI
return SpecialCardAi.LivingDeath.consider(ai, sa); return SpecialCardAi.LivingDeath.consider(ai, sa);
} } else if ("Timetwister".equals(sa.getParam("AILogic"))) {
// Timetwister AI
// Timetwister AI
if ("Timetwister".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.Timetwister.consider(ai, sa); return SpecialCardAi.Timetwister.consider(ai, sa);
} else if ("RetDiscardedThisTurn".equals(sa.getParam("AILogic"))) {
// e.g. Shadow of the Grave
return ai.getNumDiscardedThisTurn() > 0 && ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN);
} else if ("ExileGraveyards".equals(sa.getParam("AILogic"))) {
for (Player opp : ai.getOpponents()) {
CardCollectionView cardsGY = opp.getCardsIn(ZoneType.Graveyard);
CardCollection creats = CardLists.filter(cardsGY, CardPredicates.Presets.CREATURES);
if (opp.hasDelirium() || opp.hasThreshold() || creats.size() >= 5) {
return true;
}
}
return false;
} }
// TODO improve restrictions on when the AI would want to use this // TODO improve restrictions on when the AI would want to use this
@@ -130,15 +131,39 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
} }
computerType = new CardCollection(); computerType = new CardCollection();
} }
int creatureEvalThreshold = 200; // value difference (in evaluateCreatureList units)
int nonCreatureEvalThreshold = 3; // CMC difference
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
if (destination == ZoneType.Hand) {
creatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_TO_HAND_CREAT_EVAL_DIFF);
nonCreatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_TO_HAND_NONCREAT_EVAL_DIFF);
} else {
creatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_ELSEWHERE_CREAT_EVAL_DIFF);
nonCreatureEvalThreshold = aic.getIntProperty(AiProps.BOUNCE_ALL_ELSEWHERE_NONCREAT_EVAL_DIFF);
}
}
// mass zone change for creatures: if in dire danger, do it; otherwise, only do it if the opponent's
// creatures are better in value
if ((CardLists.getNotType(oppType, "Creature").size() == 0) if ((CardLists.getNotType(oppType, "Creature").size() == 0)
&& (CardLists.getNotType(computerType, "Creature").size() == 0)) { && (CardLists.getNotType(computerType, "Creature").size() == 0)) {
if ((ComputerUtilCard.evaluateCreatureList(computerType) + 200) >= ComputerUtilCard if (game.getCombat() != null && ComputerUtilCombat.lifeInSeriousDanger(ai, game.getCombat())) {
if (game.getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)
&& game.getPhaseHandler().getPlayerTurn().isOpponentOf(ai)) {
// Life is in serious danger, return all creatures from the battlefield to wherever
// so they don't deal lethal damage
return true;
}
}
if ((ComputerUtilCard.evaluateCreatureList(computerType) + creatureEvalThreshold) >= ComputerUtilCard
.evaluateCreatureList(oppType)) { .evaluateCreatureList(oppType)) {
return false; return false;
} }
} // otherwise evaluate both lists by CMC and pass only if human } // mass zone change for non-creatures: evaluate both lists by CMC and pass only if human
// permanents are more valuable // permanents are more valuable
else if ((ComputerUtilCard.evaluatePermanentList(computerType) + 3) >= ComputerUtilCard else if ((ComputerUtilCard.evaluatePermanentList(computerType) + nonCreatureEvalThreshold) >= ComputerUtilCard
.evaluatePermanentList(oppType)) { .evaluatePermanentList(oppType)) {
return false; return false;
} }
@@ -180,6 +205,14 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
// minimum card advantage unless the hand will be fully reloaded // minimum card advantage unless the hand will be fully reloaded
int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0; int minAdv = logic.contains(".minAdv") ? Integer.parseInt(logic.substring(logic.indexOf(".minAdv") + 7)) : 0;
if (numExiledWithSrc > curHandSize) {
if (ComputerUtil.predictThreatenedObjects(ai, sa, true).contains(source)) {
// Try to gain some card advantage if the card will die anyway
// TODO: ideally, should evaluate the hand value and not discard good hands to it
return true;
}
}
return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize()); return (curHandSize + minAdv - 1 < numExiledWithSrc) || (numExiledWithSrc >= ai.getMaxHandSize());
} }
} else if (origin.equals(ZoneType.Stack)) { } else if (origin.equals(ZoneType.Stack)) {
@@ -231,8 +264,8 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
* </p> * </p>
* @param sa * @param sa
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @param af * @param aiPlayer
* a {@link forge.game.ability.AbilityFactory} object. * a {@link forge.game.player.Player} object.
* *
* @return a boolean. * @return a boolean.
*/ */

View File

@@ -6,6 +6,8 @@ import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -122,7 +124,7 @@ public class ChooseCardAi extends SpellAbilityAi {
} }
} else if (aiLogic.equals("Duneblast")) { } else if (aiLogic.equals("Duneblast")) {
CardCollection aiCreatures = ai.getCreaturesInPlay(); CardCollection aiCreatures = ai.getCreaturesInPlay();
CardCollection oppCreatures = ai.getOpponent().getCreaturesInPlay(); CardCollection oppCreatures = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible"); aiCreatures = CardLists.getNotKeyword(aiCreatures, "Indestructible");
oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible"); oppCreatures = CardLists.getNotKeyword(oppCreatures, "Indestructible");

View File

@@ -6,10 +6,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.StaticData; import forge.StaticData;
import forge.ai.ComputerUtil; import forge.ai.*;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.card.CardDb; import forge.card.CardDb;
import forge.card.CardRules; import forge.card.CardRules;
import forge.card.CardSplitType; import forge.card.CardSplitType;
@@ -36,29 +33,16 @@ public class ChooseCardNameAi extends SpellAbilityAi {
String logic = sa.getParam("AILogic"); String logic = sa.getParam("AILogic");
if (logic.equals("MomirAvatar")) { if (logic.equals("MomirAvatar")) {
if (source.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.MAIN1)) { return SpecialCardAi.MomirVigAvatar.consider(ai, sa);
return false; } else if (logic.equals("CursedScroll")) {
} return SpecialCardAi.CursedScroll.consider(ai, sa);
// Set PayX here to maximum value.
int tokenSize = ComputerUtilMana.determineLeftoverMana(sa, ai);
// Some basic strategy for Momir
if (tokenSize < 2) {
return false;
}
if (tokenSize > 11) {
tokenSize = 11;
}
source.setSVar("PayX", Integer.toString(tokenSize));
} }
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) { if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(ai.getOpponent()); sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} else { } else {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} }
@@ -78,6 +62,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
*/ */
@Override @Override
public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) { public Card chooseSingleCard(final Player ai, SpellAbility sa, Iterable<Card> options, boolean isOptional, Player targetedPlayer) {
return ComputerUtilCard.getBestAI(options); return ComputerUtilCard.getBestAI(options);
} }

View File

@@ -1,6 +1,5 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
@@ -53,7 +52,7 @@ public class ChooseColorAi extends SpellAbilityAi {
} }
if ("Addle".equals(sourceName)) { if ("Addle".equals(sourceName)) {
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ai.getOpponent().getCardsIn(ZoneType.Hand).isEmpty()) { if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) || ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).isEmpty()) {
return false; return false;
} }
return true; return true;
@@ -62,7 +61,7 @@ public class ChooseColorAi extends SpellAbilityAi {
if (logic.equals("MostExcessOpponentControls")) { if (logic.equals("MostExcessOpponentControls")) {
for (byte color : MagicColor.WUBRG) { for (byte color : MagicColor.WUBRG) {
CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield); CardCollectionView ailist = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView opplist = ai.getOpponent().getCardsIn(ZoneType.Battlefield); CardCollectionView opplist = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
ailist = CardLists.filter(ailist, CardPredicates.isColor(color)); ailist = CardLists.filter(ailist, CardPredicates.isColor(color));
opplist = CardLists.filter(opplist, CardPredicates.isColor(color)); opplist = CardLists.filter(opplist, CardPredicates.isColor(color));

View File

@@ -196,7 +196,7 @@ public class ChooseGenericEffectAi extends SpellAbilityAi {
} }
// milling against Tamiyo is pointless // milling against Tamiyo is pointless
if (owner.isCardInCommand("Tamiyo, the Moon Sage emblem")) { if (owner.isCardInCommand("Emblem - Tamiyo, the Moon Sage")) {
return allow; return allow;
} }

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -16,8 +17,9 @@ public class ChooseNumberAi extends SpellAbilityAi {
TargetRestrictions tgt = sa.getTargetRestrictions(); TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (sa.canTarget(aiPlayer.getOpponent())) { Player opp = ComputerUtil.getOpponentFor(aiPlayer);
sa.getTargets().add(aiPlayer.getOpponent()); if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else { } else {
return false; return false;
} }

View File

@@ -5,6 +5,7 @@ import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
@@ -51,7 +52,7 @@ public class ChooseSourceAi extends SpellAbilityAi {
return false; return false;
} }
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false; return false;
} }
@@ -63,8 +64,9 @@ public class ChooseSourceAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (sa.canTarget(ai.getOpponent())) { Player opp = ComputerUtil.getOpponentFor(ai);
sa.getTargets().add(ai.getOpponent()); if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
} else { } else {
return false; return false;
} }

View File

@@ -1,7 +1,5 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
@@ -15,6 +13,9 @@ import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.List;
public class CloneAi extends SpellAbilityAi { public class CloneAi extends SpellAbilityAi {
@@ -131,13 +132,19 @@ public class CloneAi extends SpellAbilityAi {
* cloneTgtAI. * cloneTgtAI.
* </p> * </p>
* *
* @param af
* a {@link forge.game.ability.AbilityFactory} object.
* @param sa * @param sa
* a {@link forge.game.spellability.SpellAbility} object. * a {@link forge.game.spellability.SpellAbility} object.
* @return a boolean. * @return a boolean.
*/ */
private boolean cloneTgtAI(final SpellAbility sa) { private boolean cloneTgtAI(final SpellAbility sa) {
// Specific logic for cards
if ("CloneAttacker".equals(sa.getParam("AILogic"))) {
CardCollection valid = CardLists.getValidCards(sa.getHostCard().getController().getCardsIn(ZoneType.Battlefield), sa.getParam("ValidTgts"), sa.getHostCard().getController(), sa.getHostCard());
sa.getTargets().add(ComputerUtilCard.getBestCreatureAI(valid));
return true;
}
// Default:
// This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or // This is reasonable for now. Kamahl, Fist of Krosa and a sorcery or
// two are the only things // two are the only things
// that clone a target. Those can just use SVar:RemAIDeck:True until // that clone a target. Those can just use SVar:RemAIDeck:True until

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
CardCollection list = CardCollection list =
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on // AI won't try to grab cards that are filtered out of AI decks on
// purpose // purpose
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {

View File

@@ -227,16 +227,18 @@ public class ControlGainAi extends SpellAbilityAi {
t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true); t = ComputerUtilCard.getMostExpensivePermanentAI(list, sa, true);
} }
if (t.isCreature()) if (t != null) {
creatures--; if (t.isCreature())
if (t.isPlaneswalker()) creatures--;
planeswalkers--; if (t.isPlaneswalker())
if (t.isLand()) planeswalkers--;
lands--; if (t.isLand())
if (t.isArtifact()) lands--;
artifacts--; if (t.isArtifact())
if (t.isEnchantment()) artifacts--;
enchantments--; if (t.isEnchantment())
enchantments--;
}
if (!sa.canTarget(t)) { if (!sa.canTarget(t)) {
list.remove(t); list.remove(t);

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import forge.ai.SpecialCardAi; import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import java.util.List; import java.util.List;
@@ -25,9 +26,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) { public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
// NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through // NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
// generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there. // generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) { if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa); return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
} }
return super.chkAIDrawback(sa, aiPlayer); return super.chkAIDrawback(sa, aiPlayer);
@@ -37,5 +39,17 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) { public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
return spells.get(0); return spells.get(0);
} }
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
// Chain of Acid requires special attention here since otherwise the AI will confirm the copy and then
// run into the necessity of confirming a mandatory Destroy, thus destroying all of its own permanents.
if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(player, sa);
}
return true;
}
} }

View File

@@ -1,15 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.AiController; import forge.ai.*;
import forge.ai.AiProps;
import forge.ai.ComputerUtilAbility;
import java.util.Iterator;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
@@ -25,6 +16,8 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair; import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair; import org.apache.commons.lang3.tuple.Pair;
import java.util.Iterator;
public class CounterAi extends SpellAbilityAi { public class CounterAi extends SpellAbilityAi {
@Override @Override
@@ -43,7 +36,7 @@ public class CounterAi extends SpellAbilityAi {
if (abCost != null) { if (abCost != null) {
// AI currently disabled for these costs // AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false; return false;
} }
if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) { if (!ComputerUtilCost.checkLifeCost(ai, abCost, source, 4, sa)) {
@@ -93,8 +86,9 @@ public class CounterAi extends SpellAbilityAi {
String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null; String unlessCost = sa.hasParam("UnlessCost") ? sa.getParam("UnlessCost").trim() : null;
if (unlessCost != null && !unlessCost.endsWith(">")) { if (unlessCost != null && !unlessCost.endsWith(">")) {
// Is this Usable Mana Sources? Or Total Available Mana? Player opp = tgtSA.getActivatingPlayer();
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size(); int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
int toPay = 0; int toPay = 0;
boolean setPayX = false; boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) { if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
@@ -137,6 +131,10 @@ public class CounterAi extends SpellAbilityAi {
if (tgtCMC < minCMC) { if (tgtCMC < minCMC) {
return false; return false;
} }
} else if ("NullBrooch".equals(logic)) {
if (!SpecialCardAi.NullBrooch.consider(ai, sa)) {
return false;
}
} }
} }
@@ -146,14 +144,31 @@ public class CounterAi extends SpellAbilityAi {
boolean ctrCmc0ManaPerms = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS); boolean ctrCmc0ManaPerms = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_CMC_0_MANA_MAKING_PERMS);
boolean ctrDamageSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_DAMAGE_SPELLS); boolean ctrDamageSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_DAMAGE_SPELLS);
boolean ctrRemovalSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_REMOVAL_SPELLS); boolean ctrRemovalSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_REMOVAL_SPELLS);
boolean ctrPumpSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_PUMP_SPELLS);
boolean ctrAuraSpells = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_AURAS);
boolean ctrOtherCounters = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_OTHER_COUNTERSPELLS); boolean ctrOtherCounters = aic.getBooleanProperty(AiProps.ALWAYS_COUNTER_OTHER_COUNTERSPELLS);
int ctrChanceCMC1 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_1);
int ctrChanceCMC2 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_2);
int ctrChanceCMC3 = aic.getIntProperty(AiProps.CHANCE_TO_COUNTER_CMC_3);
String ctrNamed = aic.getProperty(AiProps.ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS); String ctrNamed = aic.getProperty(AiProps.ALWAYS_COUNTER_SPELLS_FROM_NAMED_CARDS);
boolean dontCounter = false;
if (tgtCMC == 1 && !MyRandom.percentTrue(ctrChanceCMC1)) {
dontCounter = true;
} else if (tgtCMC == 2 && !MyRandom.percentTrue(ctrChanceCMC2)) {
dontCounter = true;
} else if (tgtCMC == 3 && !MyRandom.percentTrue(ctrChanceCMC3)) {
dontCounter = true;
}
if (tgtSA != null && tgtCMC < aic.getIntProperty(AiProps.MIN_SPELL_CMC_TO_COUNTER)) { if (tgtSA != null && tgtCMC < aic.getIntProperty(AiProps.MIN_SPELL_CMC_TO_COUNTER)) {
boolean dontCounter = true; dontCounter = true;
Card tgtSource = tgtSA.getHostCard(); Card tgtSource = tgtSA.getHostCard();
if ((tgtSource != null && tgtCMC == 0 && tgtSource.isPermanent() && !tgtSource.getManaAbilities().isEmpty() && ctrCmc0ManaPerms) if ((tgtSource != null && tgtCMC == 0 && tgtSource.isPermanent() && !tgtSource.getManaAbilities().isEmpty() && ctrCmc0ManaPerms)
|| (tgtSA.getApi() == ApiType.DealDamage || tgtSA.getApi() == ApiType.LoseLife || tgtSA.getApi() == ApiType.DamageAll && ctrDamageSpells) || (tgtSA.getApi() == ApiType.DealDamage || tgtSA.getApi() == ApiType.LoseLife || tgtSA.getApi() == ApiType.DamageAll && ctrDamageSpells)
|| (tgtSA.getApi() == ApiType.Counter && ctrOtherCounters) || (tgtSA.getApi() == ApiType.Counter && ctrOtherCounters)
|| ((tgtSA.getApi() == ApiType.Pump || tgtSA.getApi() == ApiType.PumpAll) && ctrPumpSpells)
|| (tgtSA.getApi() == ApiType.Attach && ctrAuraSpells)
|| (tgtSA.getApi() == ApiType.Destroy || tgtSA.getApi() == ApiType.DestroyAll || tgtSA.getApi() == ApiType.Sacrifice || (tgtSA.getApi() == ApiType.Destroy || tgtSA.getApi() == ApiType.DestroyAll || tgtSA.getApi() == ApiType.Sacrifice
|| tgtSA.getApi() == ApiType.SacrificeAll && ctrRemovalSpells)) { || tgtSA.getApi() == ApiType.SacrificeAll && ctrRemovalSpells)) {
dontCounter = false; dontCounter = false;
@@ -167,14 +182,18 @@ public class CounterAi extends SpellAbilityAi {
} }
} }
// should always counter CMC 1 with Mental Misstep despite a possible limitation by minimum CMC // should not refrain from countering a CMC X spell if that's the only CMC
if (tgtCMC == 1 && "Mental Misstep".equals(source.getName())) { // counterable with that particular counterspell type (e.g. Mental Misstep vs. CMC 1 spells)
dontCounter = false; if (sa.getParamOrDefault("ValidTgts", "").startsWith("Card.cmcEQ")) {
int validTgtCMC = AbilityUtils.calculateAmount(source, sa.getParam("ValidTgts").substring(10), sa);
if (tgtCMC == validTgtCMC) {
dontCounter = false;
}
} }
}
if (dontCounter) { if (dontCounter) {
return false; return false;
}
} }
return toReturn; return toReturn;
@@ -212,8 +231,9 @@ public class CounterAi extends SpellAbilityAi {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
if (unlessCost != null) { if (unlessCost != null) {
// Is this Usable Mana Sources? Or Total Available Mana? Player opp = tgtSA.getActivatingPlayer();
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size(); int usableManaSources = ComputerUtilMana.getAvailableManaEstimate(opp);
int toPay = 0; int toPay = 0;
boolean setPayX = false; boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) { if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {

View File

@@ -20,6 +20,7 @@ import forge.game.combat.CombatUtil;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter; import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -225,13 +226,42 @@ public class CountersPutAi extends SpellAbilityAi {
} }
if ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) { if ("PayEnergyConservatively".equals(sa.getParam("AILogic"))) {
boolean onlyInCombat = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_IN_COMBAT);
boolean onlyDefensive = ai.getController().isAI()
&& ((PlayerControllerAi) ai.getController()).getAi().getBooleanProperty(AiProps.CONSERVATIVE_ENERGY_PAYMENT_ONLY_DEFENSIVELY);
if (playAggro) { if (playAggro) {
// aggro profiles ignore conservative play for this AI logic // aggro profiles ignore conservative play for this AI logic
return true; return true;
} else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
return true;
} else if (ai.getGame().getCombat() != null && sa.getHostCard() != null) { } else if (ai.getGame().getCombat() != null && sa.getHostCard() != null) {
if (ai.getGame().getCombat().isAttacking(sa.getHostCard())) { if (ai.getGame().getCombat().isAttacking(sa.getHostCard()) && !onlyDefensive) {
return true;
} else if (ai.getGame().getCombat().isBlocking(sa.getHostCard())) {
// when blocking, consider this if it's possible to save the blocker and/or kill at least one attacker
CardCollection blocked = ai.getGame().getCombat().getAttackersBlockedBy(sa.getHostCard());
int totBlkPower = Aggregates.sum(blocked, CardPredicates.Accessors.fnGetNetPower);
int totBlkToughness = Aggregates.min(blocked, CardPredicates.Accessors.fnGetNetToughness);
int numActivations = ai.getCounters(CounterType.ENERGY) / sa.getPayCosts().getCostEnergy().convertAmount();
if (sa.getHostCard().getNetToughness() + numActivations > totBlkPower
|| sa.getHostCard().getNetPower() + numActivations >= totBlkToughness) {
return true;
}
}
} else if (sa.getSubAbility() != null
&& "Self".equals(sa.getSubAbility().getParam("Defined"))
&& sa.getSubAbility().getParamOrDefault("KW", "").contains("Hexproof")
&& !AiCardMemory.isRememberedCard(ai, source, AiCardMemory.MemorySet.ANIMATED_THIS_TURN)) {
// Bristling Hydra: save from death using a ping activation
if (ComputerUtil.predictThreatenedObjects(sa.getActivatingPlayer(), sa).contains(source)) {
AiCardMemory.rememberCard(ai, source, AiCardMemory.MemorySet.ACTIVATED_THIS_TURN);
return true;
}
} else if (ai.getCounters(CounterType.ENERGY) > ComputerUtilCard.getMaxSAEnergyCostOnBattlefield(ai) + sa.getPayCosts().getCostEnergy().convertAmount()) {
// outside of combat, this logic only works if the relevant AI profile option is enabled
// and if there is enough energy saved
if (!onlyInCombat) {
return true; return true;
} }
} }
@@ -326,7 +356,7 @@ public class CountersPutAi extends SpellAbilityAi {
PhaseHandler ph = ai.getGame().getPhaseHandler(); PhaseHandler ph = ai.getGame().getPhaseHandler();
if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) { if ("AlwaysAtOppEOT".equals(sa.getParam("AILogic"))) {
if (ph.is(PhaseType.END_OF_TURN) && !ph.isPlayerTurn(ai)) { if (ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai)) {
return true; return true;
} }
} }
@@ -373,6 +403,13 @@ public class CountersPutAi extends SpellAbilityAi {
} }
}); });
if (abCost.hasSpecificCostType(CostSacrifice.class)) {
Card sacTarget = ComputerUtil.getCardPreference(ai, source, "SacCost", list);
// this card is planned to be sacrificed during cost payment, so don't target it
// (otherwise the AI can cheat by activating this SA and not paying the sac cost, e.g. Extruder)
list.remove(sacTarget);
}
if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) { if (list.size() < sa.getTargetRestrictions().getMinTargets(source, sa)) {
return false; return false;
} }
@@ -491,7 +528,7 @@ public class CountersPutAi extends SpellAbilityAi {
boolean immediately = ComputerUtil.playImmediately(ai, sa); boolean immediately = ComputerUtil.playImmediately(ai, sa);
if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, immediately)) { if (abCost != null && !ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, immediately)) {
return false; return false;
} }
@@ -670,6 +707,13 @@ public class CountersPutAi extends SpellAbilityAi {
list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa); list = CardLists.getTargetableCards(ai.getOpponents().getCardsIn(ZoneType.Battlefield), sa);
preferred = false; preferred = false;
} }
if (list.isEmpty()) {
// Still an empty list, but we have to choose something (mandatory); expand targeting to
// include AI's own cards to see if there's anything targetable (e.g. Plague Belcher).
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
preferred = false;
}
} }
if (list.isEmpty()) { if (list.isEmpty()) {

View File

@@ -1,6 +1,8 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -35,10 +37,11 @@ public class CountersPutAllAi extends SpellAbilityAi {
final String type = sa.getParam("CounterType"); final String type = sa.getParam("CounterType");
final String amountStr = sa.getParam("CounterNum"); final String amountStr = sa.getParam("CounterNum");
final String valid = sa.getParam("ValidCards"); final String valid = sa.getParam("ValidCards");
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean curse = sa.isCurse(); final boolean curse = sa.isCurse();
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
hList = CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid, source.getController(), source); hList = CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source); cList = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
if (abCost != null) { if (abCost != null) {
@@ -51,13 +54,19 @@ public class CountersPutAllAi extends SpellAbilityAi {
return false; return false;
} }
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false;
}
}
if (logic.equals("AtEOTOrBlock")) {
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false; return false;
} }
} }
if (tgt != null) { if (tgt != null) {
Player pl = curse ? ai.getOpponent() : ai; Player pl = curse ? ComputerUtil.getOpponentFor(ai) : ai;
sa.getTargets().add(pl); sa.getTargets().add(pl);
hList = CardLists.filterControlledBy(hList, pl); hList = CardLists.filterControlledBy(hList, pl);
@@ -138,6 +147,6 @@ public class CountersPutAllAi extends SpellAbilityAi {
*/ */
@Override @Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return player.getCreaturesInPlay().size() >= player.getOpponent().getCreaturesInPlay().size(); return player.getCreaturesInPlay().size() >= ComputerUtil.getOpponentFor(player).getCreaturesInPlay().size();
} }
} }

View File

@@ -1,7 +1,7 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
@@ -18,7 +18,14 @@ public abstract class DamageAiBase extends SpellAbilityAi {
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) { protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
int restDamage = d; int restDamage = d;
final Game game = comp.getGame(); final Game game = comp.getGame();
final Player enemy = comp.getOpponent(); Player enemy = ComputerUtil.getOpponentFor(comp);
boolean dmgByCardsInHand = false;
if ("X".equals(sa.getParam("NumDmg")) && sa.getHostCard() != null && sa.hasSVar(sa.getParam("NumDmg")) &&
sa.getHostCard().getSVar(sa.getParam("NumDmg")).equals("TargetedPlayer$CardsInHand")) {
dmgByCardsInHand = true;
}
if (!sa.canTarget(enemy)) { if (!sa.canTarget(enemy)) {
return false; return false;
} }
@@ -70,8 +77,11 @@ public abstract class DamageAiBase extends SpellAbilityAi {
value = 1.0f * restDamage / enemy.getLife(); value = 1.0f * restDamage / enemy.getLife();
} }
} else { } else {
if (phase.isPlayerTurn(enemy) && phase.is(PhaseType.END_OF_TURN)) { if (phase.isPlayerTurn(enemy)) {
value = 1.5f * restDamage / enemy.getLife(); if (phase.is(PhaseType.END_OF_TURN)
|| ((dmgByCardsInHand && phase.getPhase().isAfter(PhaseType.UPKEEP)))) {
value = 1.5f * restDamage / enemy.getLife();
}
} }
} }
if (value > 0) { //more likely to burn with larger hand if (value > 0) { //more likely to burn with larger hand

View File

@@ -1,7 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import forge.ai.*; import forge.ai.*;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -56,16 +55,35 @@ public class DamageAllAi extends SpellAbilityAi {
x = source.getCounters(CounterType.LOYALTY); x = source.getCounters(CounterType.LOYALTY);
} }
if (x == -1) { if (x == -1) {
Player bestOpp = determineOppToKill(ai, sa, source, dmg);
if (determineOppToKill(ai, sa, source, dmg) != null) {
// we already know we can kill a player, so go for it
return true;
}
// look for other value in this (damaging creatures or
// creatures + player, e.g. Pestilence, etc.)
return evaluateDamageAll(ai, sa, source, dmg) > 0; return evaluateDamageAll(ai, sa, source, dmg) > 0;
} else { } else {
int best = -1, best_x = -1; int best = -1, best_x = -1;
for (int i = 0; i < x; i++) { Player bestOpp = determineOppToKill(ai, sa, source, x);
final int value = evaluateDamageAll(ai, sa, source, i); if (bestOpp != null) {
if (value > best) { // we can finish off a player, so go for it
best = value;
best_x = i; // TODO: improve this by possibly damaging more creatures
// on the battlefield belonging to other opponents at the same
// time, if viable
best_x = bestOpp.getLife();
} else {
// see if it's possible to get value from killing off creatures
for (int i = 0; i <= x; i++) {
final int value = evaluateDamageAll(ai, sa, source, i);
if (value > best) {
best = value;
best_x = i;
}
} }
} }
if (best_x > 0) { if (best_x > 0) {
if (sa.getSVar(damage).equals("Count$xPaid")) { if (sa.getSVar(damage).equals("Count$xPaid")) {
source.setSVar("PayX", Integer.toString(best_x)); source.setSVar("PayX", Integer.toString(best_x));
@@ -79,8 +97,31 @@ public class DamageAllAi extends SpellAbilityAi {
} }
} }
private Player determineOppToKill(Player ai, SpellAbility sa, Card source, int x) {
// Attempt to determine which opponent can be finished off such that the most players
// are killed at the same time, given X damage tops
final String validP = sa.hasParam("ValidPlayers") ? sa.getParam("ValidPlayers") : "";
int aiLife = ai.getLife();
Player bestOpp = null; // default opponent, if all else fails
for (int dmg = 1; dmg <= x; dmg++) {
// Don't kill yourself in the process
if (validP.equals("Player") && aiLife <= ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)) {
break;
}
for (Player opp : ai.getOpponents()) {
if ((validP.equals("Player") || validP.contains("Opponent"))
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
bestOpp = opp;
}
}
}
return bestOpp;
}
private int evaluateDamageAll(Player ai, SpellAbility sa, final Card source, int dmg) { private int evaluateDamageAll(Player ai, SpellAbility sa, final Card source, int dmg) {
Player opp = ai.getOpponent(); final Player opp = ai.getWeakestOpponent();
final CardCollection humanList = getKillableCreatures(sa, opp, dmg); final CardCollection humanList = getKillableCreatures(sa, opp, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg); CardCollection computerList = getKillableCreatures(sa, ai, dmg);
@@ -98,62 +139,56 @@ public class DamageAllAi extends SpellAbilityAi {
return -1; return -1;
} }
// if we can kill human, do it int minGain = 200; // The minimum gain in destroyed creatures
if ((validP.equals("Player") || validP.contains("Opponent")) if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) {
&& (opp.getLife() <= ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) { if (computerList.isEmpty()) {
return 1; minGain = 10; // nothing to lose
} // no creatures to lose and player can be damaged
// so do it if it's helping!
int minGain = 200; // The minimum gain in destroyed creatures // ----------------------------
if (sa.getPayCosts() != null && sa.getPayCosts().isReusuableResource()) { // needs future improvement on pestilence :
if (computerList.isEmpty()) { // what if we lose creatures but can win by repeated activations?
minGain = 10; // nothing to lose // that tactic only works if there are creatures left to keep pestilence in play
// no creatures to lose and player can be damaged // and can kill the player in a reasonable amount of time (no more than 2-3 turns?)
// so do it if it's helping! if (validP.equals("Player")) {
// ---------------------------- if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) {
// needs future improvement on pestilence : // When using Pestilence to hurt players, do it at
// what if we lose creatures but can win by repeated activations? // the end of the opponent's turn only
// that tactic only works if there are creatures left to keep pestilence in play if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic")))
// and can kill the player in a reasonable amount of time (no more than 2-3 turns?) || ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN)
if (validP.equals("Player")) { && (ai.getGame().getNonactivePlayers().contains(ai)))))
if (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) > 0) { // Need further improvement : if able to kill immediately with repeated activations, do not wait
// When using Pestilence to hurt players, do it at // for phases! Will also need to implement considering repeated activations for killed creatures!
// the end of the opponent's turn only // || (ai.sa.getPayCosts(). ??? )
if ((!"DmgAllCreaturesAndPlayers".equals(sa.getParam("AILogic"))) {
|| ((ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) // would take zero damage, and hurt opponent, do it!
&& (ai.getGame().getNonactivePlayers().contains(ai))))) if (ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)<1) {
// Need further improvement : if able to kill immediately with repeated activations, do not wait return 1;
// for phases! Will also need to implement considering repeated activations for killed creatures! }
// || (ai.sa.getPayCosts(). ??? ) // enemy is expected to die faster than AI from damage if repeated
{ if (ai.getLife() > ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)
// would take zero damage, and hurt opponent, do it! * ((opp.getLife() + ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) - 1)
if (ComputerUtilCombat.predictDamageTo(ai, dmg, source, false)<1) { / ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) {
return 1; // enemy below 10 life, go for it!
} if ((opp.getLife() < 10)
// enemy is expected to die faster than AI from damage if repeated && (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) >= 1)) {
if (ai.getLife() > ComputerUtilCombat.predictDamageTo(ai, dmg, source, false) return 1;
* ((opp.getLife() + ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) - 1) }
/ ComputerUtilCombat.predictDamageTo(opp, dmg, source, false))) { // At least half enemy remaining life can be removed in one go
// enemy below 10 life, go for it! // worth doing even if enemy still has high health - one more copy of spell to win!
if ((opp.getLife() < 10) if (opp.getLife() <= 2 * ComputerUtilCombat.predictDamageTo(opp, dmg, source, false)) {
&& (ComputerUtilCombat.predictDamageTo(opp, dmg, source, false) >= 1)) { return 1;
return 1; }
} }
// At least half enemy remaining life can be removed in one go }
// worth doing even if enemy still has high health - one more copy of spell to win! }
if (opp.getLife() <= 2 * ComputerUtilCombat.predictDamageTo(opp, dmg, source, false)) { }
return 1; } else {
} minGain = 100; // safety for errors in evaluate creature
} }
}
}
}
} else {
minGain = 100; // safety for errors in evaluate creature
}
} else if (sa.getSubAbility() != null && ai.getGame().getPhaseHandler().isPreCombatMain() && computerList.isEmpty() } else if (sa.getSubAbility() != null && ai.getGame().getPhaseHandler().isPreCombatMain() && computerList.isEmpty()
&& opp.getCreaturesInPlay().size() > 1 && !ai.getCreaturesInPlay().isEmpty()) { && opp.getCreaturesInPlay().size() > 1 && !ai.getCreaturesInPlay().isEmpty()) {
minGain = 126; // prepare for attack minGain = 126; // prepare for attack
} }
return ComputerUtilCard.evaluateCreatureList(humanList) - ComputerUtilCard.evaluateCreatureList(computerList) return ComputerUtilCard.evaluateCreatureList(humanList) - ComputerUtilCard.evaluateCreatureList(computerList)
@@ -179,7 +214,7 @@ public class DamageAllAi extends SpellAbilityAi {
} }
// Evaluate creatures getting killed // Evaluate creatures getting killed
Player enemy = ai.getOpponent(); Player enemy = ComputerUtil.getOpponentFor(ai);
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg); final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg); CardCollection computerList = getKillableCreatures(sa, ai, dmg);
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -261,7 +296,7 @@ public class DamageAllAi extends SpellAbilityAi {
} }
// Evaluate creatures getting killed // Evaluate creatures getting killed
Player enemy = ai.getOpponent(); Player enemy = ComputerUtil.getOpponentFor(ai);
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg); final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg); CardCollection computerList = getKillableCreatures(sa, ai, dmg);
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();

View File

@@ -1,21 +1,13 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import java.util.Map;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import com.google.common.collect.Maps; import com.google.common.collect.Maps;
import forge.ai.*; import forge.ai.*;
import forge.game.Game; import forge.game.Game;
import forge.game.GameObject; import forge.game.GameObject;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -28,6 +20,9 @@ import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates; import forge.util.Aggregates;
import java.util.List;
import java.util.Map;
public class DamageDealAi extends DamageAiBase { public class DamageDealAi extends DamageAiBase {
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) { public boolean chkAIDrawback(SpellAbility sa, Player ai) {
@@ -98,6 +93,29 @@ public class DamageDealAi extends DamageAiBase {
source.setSVar("PayX", Integer.toString(dmg)); source.setSVar("PayX", Integer.toString(dmg));
} else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) { } else if (sa.getSVar(damage).equals("Count$CardsInYourHand") && source.getZone().is(ZoneType.Hand)) {
dmg--; // the card will be spent casting the spell, so actual damage is 1 less dmg--; // the card will be spent casting the spell, so actual damage is 1 less
} else if (sa.getSVar(damage).equals("TargetedPlayer$CardsInHand")) {
// cards that deal damage by the number of cards in target player's hand, e.g. Sudden Impact
if (sa.getTargetRestrictions().canTgtPlayer()) {
int maxDmg = 0;
Player maxDamaged = null;
for (Player p : ai.getOpponents()) {
if (p.canBeTargetedBy(sa)) {
if (p.getCardsIn(ZoneType.Hand).size() > maxDmg) {
maxDmg = p.getCardsIn(ZoneType.Hand).size();
maxDamaged = p;
}
}
}
if (maxDmg > 0 && maxDamaged != null) {
if (shouldTgtP(ai, sa, maxDmg, false)) {
sa.resetTargets();
sa.getTargets().add(maxDamaged);
return true;
}
} else {
return false;
}
}
} }
} }
@@ -105,9 +123,20 @@ public class DamageDealAi extends DamageAiBase {
dmg += 2; dmg += 2;
} }
String logic = sa.getParam("AILogic"); String logic = sa.getParamOrDefault("AILogic", "");
if ("DiscardLands".equals(logic)) { if ("DiscardLands".equals(logic)) {
dmg = 2; dmg = 2;
} else if (logic.startsWith("ProcRaid.")) {
if (ai.getGame().getPhaseHandler().isPlayerTurn(ai) && ai.getGame().getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
for (Card potentialAtkr : ai.getCreaturesInPlay()) {
if (ComputerUtilCard.doesCreatureAttackAI(ai, potentialAtkr)) {
return false;
}
}
}
if (ai.getAttackedWithCreatureThisTurn()) {
dmg = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
}
} else if ("WildHunt".equals(logic)) { } else if ("WildHunt".equals(logic)) {
// This dummy ability will just deal 0 damage, but holds the logic for the AI for Master of Wild Hunt // This dummy ability will just deal 0 damage, but holds the logic for the AI for Master of Wild Hunt
List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source); List<Card> wolves = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), "Creature.Wolf+untapped+YouCtrl+Other", ai, source);
@@ -157,7 +186,7 @@ public class DamageDealAi extends DamageAiBase {
return false; return false;
} }
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false; return false;
} }
@@ -345,7 +374,7 @@ public class DamageDealAi extends DamageAiBase {
final boolean divided = sa.hasParam("DividedAsYouChoose"); final boolean divided = sa.hasParam("DividedAsYouChoose");
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer"); final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
Player enemy = ai.getOpponent(); Player enemy = ComputerUtil.getOpponentFor(ai);
if ("PowerDmg".equals(sa.getParam("AILogic"))) { if ("PowerDmg".equals(sa.getParam("AILogic"))) {
// check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI() // check if it is better to target the player instead, the original target is already set in PumpAi.pumpTgtAI()
@@ -377,7 +406,7 @@ public class DamageDealAi extends DamageAiBase {
} }
if ("Polukranos".equals(sa.getParam("AILogic"))) { if ("Polukranos".equals(sa.getParam("AILogic"))) {
int dmgTaken = 0; int dmgTaken = 0;
CardCollection humCreatures = ai.getOpponent().getCreaturesInPlay(); CardCollection humCreatures = enemy.getCreaturesInPlay();
Card lastTgt = null; Card lastTgt = null;
humCreatures = CardLists.getTargetableCards(humCreatures, sa); humCreatures = CardLists.getTargetableCards(humCreatures, sa);
ComputerUtilCard.sortByEvaluateCreature(humCreatures); ComputerUtilCard.sortByEvaluateCreature(humCreatures);
@@ -633,7 +662,7 @@ public class DamageDealAi extends DamageAiBase {
// this is for Triggered targets that are mandatory // this is for Triggered targets that are mandatory
final boolean noPrevention = sa.hasParam("NoPrevention"); final boolean noPrevention = sa.hasParam("NoPrevention");
final boolean divided = sa.hasParam("DividedAsYouChoose"); final boolean divided = sa.hasParam("DividedAsYouChoose");
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
System.out.println("damageChooseRequiredTargets " + ai + " " + sa); System.out.println("damageChooseRequiredTargets " + ai + " " + sa);
while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) { while (sa.getTargets().getNumTargeted() < tgt.getMinTargets(sa.getHostCard(), sa)) {

View File

@@ -3,10 +3,6 @@ package forge.ai.ability;
import forge.ai.SpecialCardAi; import forge.ai.SpecialCardAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
import forge.game.player.PlayerPredicates; import forge.game.player.PlayerPredicates;

View File

@@ -42,7 +42,7 @@ public class DamagePreventAi extends SpellAbilityAi {
return false; return false;
} }
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) { if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
return false; return false;
} }

View File

@@ -30,7 +30,7 @@ public class DamagePreventAllAi extends SpellAbilityAi {
return false; return false;
} }
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) { if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
return false; return false;
} }

View File

@@ -4,6 +4,7 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -42,7 +43,7 @@ public class DebuffAi extends SpellAbilityAi {
final Cost cost = sa.getPayCosts(); final Cost cost = sa.getPayCosts();
// temporarily disabled until AI is improved // temporarily disabled until AI is improved
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source)) { if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
return false; return false;
} }
@@ -176,7 +177,7 @@ public class DebuffAi extends SpellAbilityAi {
* @return a CardCollection. * @return a CardCollection.
*/ */
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) { private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa); CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
if (!list.isEmpty()) { if (!list.isEmpty()) {
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {
@@ -216,7 +217,7 @@ public class DebuffAi extends SpellAbilityAi {
list.remove(c); list.remove(c);
} }
final CardCollection pref = CardLists.filterControlledBy(list, ai.getOpponent()); final CardCollection pref = CardLists.filterControlledBy(list, ComputerUtil.getOpponentFor(ai));
final CardCollection forced = CardLists.filterControlledBy(list, ai); final CardCollection forced = CardLists.filterControlledBy(list, ai);
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();

View File

@@ -1,24 +1,10 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import forge.ai.AiController; import forge.ai.*;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostSacrifice; import forge.game.cost.CostSacrifice;
@@ -49,7 +35,7 @@ public class DestroyAi extends SpellAbilityAi {
CardCollection list; CardCollection list;
if (abCost != null) { if (abCost != null) {
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false; return false;
} }
@@ -78,6 +64,11 @@ public class DestroyAi extends SpellAbilityAi {
} }
if ("MadSarkhanDragon".equals(logic)) { if ("MadSarkhanDragon".equals(logic)) {
return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa); return SpecialCardAi.SarkhanTheMad.considerMakeDragon(ai, sa);
} else if (logic != null && logic.startsWith("MinLoyalty.")) {
int minLoyalty = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
if (source.getCounters(CounterType.LOYALTY) < minLoyalty) {
return false;
}
} else if ("Polymorph".equals(logic)) { } else if ("Polymorph".equals(logic)) {
list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa); list = CardLists.getTargetableCards(ai.getCardsIn(ZoneType.Battlefield), sa);
if (list.isEmpty()) { if (list.isEmpty()) {
@@ -267,7 +258,7 @@ public class DestroyAi extends SpellAbilityAi {
} else if (sa.hasParam("Defined")) { } else if (sa.hasParam("Defined")) {
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa); list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai) if ("WillSkipTurn".equals(logic) && (sa.getHostCard().getController().equals(ai)
|| ai.getCreaturesInPlay().size() < ai.getOpponent().getCreaturesInPlay().size() || ai.getCreaturesInPlay().size() < ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size()
|| !source.getGame().getPhaseHandler().isPlayerTurn(ai) || !source.getGame().getPhaseHandler().isPlayerTurn(ai)
|| ai.getLife() <= 5)) { || ai.getLife() <= 5)) {
// Basic ai logic for Lethal Vapors // Basic ai logic for Lethal Vapors

View File

@@ -64,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi {
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) { public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
Player opponent = ai.getOpponent(); // TODO: how should this AI logic work for multiplayer and getOpponents()? Player opponent = ComputerUtil.getOpponentFor(ai); // TODO: how should this AI logic work for multiplayer and getOpponents()?
final int CREATURE_EVAL_THRESHOLD = 200; final int CREATURE_EVAL_THRESHOLD = 200;

View File

@@ -9,7 +9,6 @@ import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterType;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
@@ -26,7 +25,7 @@ public class DigAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Game game = ai.getGame(); final Game game = ai.getGame();
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
Player libraryOwner = ai; Player libraryOwner = ai;
@@ -103,7 +102,7 @@ public class DigAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();
if (mandatory && sa.canTarget(opp)) { if (mandatory && sa.canTarget(opp)) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.Card;
@@ -20,6 +21,7 @@ public class DigUntilAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
Card source = sa.getHostCard(); Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
double chance = .4; // 40 percent chance with instant speed stuff double chance = .4; // 40 percent chance with instant speed stuff
if (SpellAbilityAi.isSorcerySpeed(sa)) { if (SpellAbilityAi.isSorcerySpeed(sa)) {
chance = .667; // 66.7% chance for sorcery speed (since it will chance = .667; // 66.7% chance for sorcery speed (since it will
@@ -29,7 +31,22 @@ public class DigUntilAi extends SpellAbilityAi {
final boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1); final boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
Player libraryOwner = ai; Player libraryOwner = ai;
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
if ("DontMillSelf".equals(logic)) {
// A card that digs for specific things and puts everything revealed before it into graveyard
// (e.g. Hermit Druid) - don't use it to mill itself and also make sure there's enough playable
// material in the library after using it several times.
// TODO: maybe this should happen for any DigUntil SA with RevealedDestination$ Graveyard?
if (ai.getCardsIn(ZoneType.Library).size() < 20) {
return false;
}
if ("Land.Basic".equals(sa.getParam("Valid"))
&& !CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS_PRODUCING_MANA).isEmpty()) {
// We already have a mana-producing land in hand, so bail
return false;
}
}
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
sa.resetTargets(); sa.resetTargets();

View File

@@ -1,10 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil; import forge.ai.*;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
@@ -27,10 +23,11 @@ public class DiscardAi extends SpellAbilityAi {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
final Cost abCost = sa.getPayCosts(); final Cost abCost = sa.getPayCosts();
final String aiLogic = sa.getParamOrDefault("AILogic", "");
if (abCost != null) { if (abCost != null) {
// AI currently disabled for these costs // AI currently disabled for these costs
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa)) {
return false; return false;
} }
@@ -53,7 +50,11 @@ public class DiscardAi extends SpellAbilityAi {
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand)); return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
} }
final boolean humanHasHand = ai.getOpponent().getCardsIn(ZoneType.Hand).size() > 0; if (aiLogic.equals("VolrathsShapeshifter")) {
return SpecialCardAi.VolrathsShapeshifter.consider(ai, sa);
}
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
if (tgt != null) { if (tgt != null) {
if (!discardTargetAI(ai, sa)) { if (!discardTargetAI(ai, sa)) {
@@ -84,7 +85,7 @@ public class DiscardAi extends SpellAbilityAi {
if (sa.hasParam("NumCards")) { if (sa.hasParam("NumCards")) {
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) { if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. // Set PayX here to maximum value.
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getOpponent() final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
.getCardsIn(ZoneType.Hand).size()); .getCardsIn(ZoneType.Hand).size());
if (cardsToDiscard < 1) { if (cardsToDiscard < 1) {
return false; return false;
@@ -120,7 +121,7 @@ public class DiscardAi extends SpellAbilityAi {
private boolean discardTargetAI(final Player ai, final SpellAbility sa) { private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) { if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
return false; return false;
} }
@@ -139,7 +140,7 @@ public class DiscardAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
if (!discardTargetAI(ai, sa)) { if (!discardTargetAI(ai, sa)) {
if (mandatory && sa.canTarget(opp)) { if (mandatory && sa.canTarget(opp)) {
sa.getTargets().add(opp); sa.getTargets().add(opp);
@@ -160,7 +161,7 @@ public class DiscardAi extends SpellAbilityAi {
} }
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) { if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
// Set PayX here to maximum value. // Set PayX here to maximum value.
final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ai.getOpponent() final int cardsToDiscard = Math.min(ComputerUtilMana.determineLeftoverMana(sa, ai), ComputerUtil.getOpponentFor(ai)
.getCardsIn(ZoneType.Hand).size()); .getCardsIn(ZoneType.Hand).size());
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard)); sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
} }

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -19,7 +20,7 @@ public class DrainManaAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final Random r = MyRandom.getRandom(); final Random r = MyRandom.getRandom();
boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn()); boolean randomReturn = r.nextFloat() <= Math.pow(.6667, sa.getActivationsThisTurn());
@@ -42,7 +43,7 @@ public class DrainManaAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
@@ -83,7 +84,7 @@ public class DrainManaAi extends SpellAbilityAi {
} }
} else { } else {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(ai.getOpponent()); sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} }
return randomReturn; return randomReturn;

View File

@@ -86,7 +86,7 @@ public class DrawAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) { protected boolean willPayCosts(Player ai, SpellAbility sa, Cost cost, Card source) {
if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source)) { if (!ComputerUtilCost.checkCreatureSacrificeCost(ai, cost, source, sa)) {
return false; return false;
} }
@@ -126,6 +126,16 @@ public class DrawAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) { protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
String logic = sa.getParamOrDefault("AILogic", "");
if (logic.startsWith("LifeLessThan.")) {
// LifeLessThan logic presupposes activation as soon as possible in an
// attempt to save the AI from dying
return true;
} else if (logic.equals("AtEndOfOppTurn")) {
return ph.is(PhaseType.END_OF_TURN) && ph.getNextTurn().equals(ai);
}
// Don't use draw abilities before main 2 if possible // Don't use draw abilities before main 2 if possible
if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") if (ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa)) { && !ComputerUtil.castSpellInMain1(ai, sa)) {
@@ -204,6 +214,7 @@ public class DrawAi extends SpellAbilityAi {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final boolean drawback = sa.getParent() != null; final boolean drawback = sa.getParent() != null;
final Game game = ai.getGame(); final Game game = ai.getGame();
final String logic = sa.getParamOrDefault("AILogic", "");
int computerHandSize = ai.getCardsIn(ZoneType.Hand).size(); int computerHandSize = ai.getCardsIn(ZoneType.Hand).size();
final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size(); final int computerLibrarySize = ai.getCardsIn(ZoneType.Library).size();
@@ -241,10 +252,8 @@ public class DrawAi extends SpellAbilityAi {
} }
// Logic for cards that require special handling // Logic for cards that require special handling
if (sa.hasParam("AILogic")) { if ("YawgmothsBargain".equals(logic)) {
if ("YawgmothsBargain".equals(sa.getParam("AILogic"))) { return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
return SpecialCardAi.YawgmothsBargain.consider(ai, sa);
}
} }
// Generic logic for all cards that do not need any special handling // Generic logic for all cards that do not need any special handling
@@ -312,6 +321,13 @@ public class DrawAi extends SpellAbilityAi {
return true; return true;
} }
} }
// we're trying to save ourselves from death
// (e.g. Bargain), so target the opp anyway
if (logic.startsWith("LifeLessThan.")) {
int threshold = Integer.parseInt(logic.substring(logic.indexOf(".") + 1));
sa.getTargets().add(oppA);
return ai.getLife() < threshold;
}
} }
boolean aiTarget = sa.canTarget(ai); boolean aiTarget = sa.canTarget(ai);

View File

@@ -4,6 +4,7 @@ import java.util.List;
import java.util.Random; import java.util.Random;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
@@ -15,8 +16,7 @@ import forge.ai.SpellApiToAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GlobalRuleChange; import forge.game.GlobalRuleChange;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardLists;
import forge.game.combat.CombatUtil; import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -48,6 +48,21 @@ public class EffectAi extends SpellAbilityAi {
return false; return false;
} }
randomReturn = true; randomReturn = true;
} else if (logic.equals("KeepOppCreatsLandsTapped")) {
for (Player opp : ai.getOpponents()) {
boolean worthHolding = false;
CardCollectionView oppCreatsLands = CardLists.filter(opp.getCardsIn(ZoneType.Battlefield),
Predicates.or(CardPredicates.Presets.LANDS, CardPredicates.Presets.CREATURES));
CardCollectionView oppCreatsLandsTapped = CardLists.filter(oppCreatsLands, CardPredicates.Presets.TAPPED);
if (oppCreatsLandsTapped.size() >= 3 || oppCreatsLands.size() == oppCreatsLandsTapped.size()) {
worthHolding = true;
}
if (!worthHolding) {
return false;
}
randomReturn = true;
}
} else if (logic.equals("Fog")) { } else if (logic.equals("Fog")) {
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) { if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer())) {
return false; return false;
@@ -179,9 +194,7 @@ public class EffectAi extends SpellAbilityAi {
break; break;
} }
if (shouldPlay) { return shouldPlay;
return true;
}
} else if (logic.equals("RedirectSpellDamageFromPlayer")) { } else if (logic.equals("RedirectSpellDamageFromPlayer")) {
if (game.getStack().isEmpty()) { if (game.getStack().isEmpty()) {
return false; return false;
@@ -246,6 +259,12 @@ public class EffectAi extends SpellAbilityAi {
} }
} }
return true; return true;
} else if (logic.equals("CastFromGraveThisTurn")) {
CardCollection list = new CardCollection(game.getCardsIn(ZoneType.Graveyard));
list = CardLists.getValidCards(list, sa.getTargetRestrictions().getValidTgts(), ai, sa.getHostCard(), sa);
if (!ComputerUtil.targetPlayableSpellCard(ai, list, sa, false)) {
return false;
}
} }
} else { //no AILogic } else { //no AILogic
return false; return false;
@@ -281,4 +300,35 @@ public class EffectAi extends SpellAbilityAi {
return randomReturn; return randomReturn;
} }
@Override
protected boolean doTriggerAINoCost(final Player aiPlayer, final SpellAbility sa, final boolean mandatory) {
String aiLogic = sa.getParamOrDefault("AILogic", "");
// E.g. Nova Pentacle
if (aiLogic.equals("RedirectFromOppToCreature")) {
// try to target the opponent's best targetable permanent, if able
CardCollection oppPerms = CardLists.getValidCards(aiPlayer.getOpponents().getCardsIn(ZoneType.Battlefield), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
if (!oppPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getBestAI(oppPerms));
return true;
}
if (mandatory) {
// try to target the AI's worst targetable permanent, if able
CardCollection aiPerms = CardLists.getValidCards(aiPlayer.getCardsIn(ZoneType.Battlefield), sa.getTargetRestrictions().getValidTgts(), aiPlayer, sa.getHostCard(), sa);
if (!aiPerms.isEmpty()) {
sa.resetTargets();
sa.getTargets().add(ComputerUtilCard.getWorstAI(aiPerms));
return true;
}
}
return false;
}
return super.doTriggerAINoCost(aiPlayer, sa, mandatory);
}
} }

View File

@@ -0,0 +1,52 @@
package forge.ai.ability;
import forge.ai.*;
import forge.game.card.*;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
public class ExploreAi extends SpellAbilityAi {
/* (non-Javadoc)
* @see forge.card.abilityfactory.SpellAiLogic#canPlayAI(forge.game.player.Player, java.util.Map, forge.card.spellability.SpellAbility)
*/
@Override
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
return true;
}
public static Card shouldPutInGraveyard(CardCollection top, Player ai) {
int predictedMana = ComputerUtilMana.getAvailableManaSources(ai, false).size();
CardCollectionView cardsOTB = ai.getCardsIn(ZoneType.Battlefield);
CardCollectionView cardsInHand = ai.getCardsIn(ZoneType.Hand);
CardCollection landsOTB = CardLists.filter(cardsOTB, CardPredicates.Presets.LANDS_PRODUCING_MANA);
CardCollection landsInHand = CardLists.filter(cardsInHand, CardPredicates.Presets.LANDS_PRODUCING_MANA);
int maxCMCDiff = 1;
int numLandsToStillNeedMore = 2;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
maxCMCDiff = aic.getIntProperty(AiProps.EXPLORE_MAX_CMC_DIFF_TO_PUT_IN_GRAVEYARD);
numLandsToStillNeedMore = aic.getIntProperty(AiProps.EXPLORE_NUM_LANDS_TO_STILL_NEED_MORE);
}
if (!top.isEmpty()) {
Card topCard = top.getFirst();
if (landsInHand.isEmpty() && landsOTB.size() <= numLandsToStillNeedMore) {
// We need more lands to improve our mana base, explore away the non-lands
return topCard;
}
if (topCard.getCMC() - maxCMCDiff >= predictedMana && !topCard.hasSVar("DoNotDiscardIfAble")) {
// We're not casting this in foreseeable future, put it in the graveyard
return topCard;
}
}
// Put on top of the library (do not mark the card for placement in the graveyard)
return null;
}
}

View File

@@ -1,12 +1,16 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.card.Card;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.util.Aggregates;
public class FogAi extends SpellAbilityAi { public class FogAi extends SpellAbilityAi {
@@ -34,6 +38,25 @@ public class FogAi extends SpellAbilityAi {
return false; return false;
} }
if ("SeriousDamage".equals(sa.getParam("AILogic")) && game.getCombat() != null) {
int dmg = 0;
for (Card atk : game.getCombat().getAttackersOf(ai)) {
if (game.getCombat().isUnblocked(atk)) {
dmg += atk.getNetCombatDamage();
} else if (atk.hasKeyword("Trample")) {
dmg += atk.getNetCombatDamage() - Aggregates.sum(game.getCombat().getBlockers(atk), CardPredicates.Accessors.fnGetNetToughness);
}
}
if (dmg > ai.getLife() / 4) {
return true;
} else if (dmg >= 5) {
return true;
} else if (ai.getLife() < ai.getStartingLife() / 3) {
return true;
}
}
// Cast it if life is in danger // Cast it if life is in danger
return ComputerUtilCombat.lifeInDanger(ai, game.getCombat()); return ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
} }
@@ -58,7 +81,7 @@ public class FogAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
final Game game = aiPlayer.getGame(); final Game game = aiPlayer.getGame();
boolean chance; boolean chance;
if (game.getPhaseHandler().isPlayerTurn(sa.getActivatingPlayer().getOpponent())) { if (game.getPhaseHandler().isPlayerTurn(ComputerUtil.getOpponentFor(sa.getActivatingPlayer()))) {
chance = game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE); chance = game.getPhaseHandler().getPhase().isBefore(PhaseType.COMBAT_FIRST_STRIKE_DAMAGE);
} else { } else {
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE); chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -8,7 +9,7 @@ import forge.game.spellability.TargetRestrictions;
public class GameLossAi extends SpellAbilityAi { public class GameLossAi extends SpellAbilityAi {
@Override @Override
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
if (opp.cantLose()) { if (opp.cantLose()) {
return false; return false;
} }
@@ -34,14 +35,14 @@ public class GameLossAi extends SpellAbilityAi {
// (Final Fortune would need to attach it's delayed trigger to a // (Final Fortune would need to attach it's delayed trigger to a
// specific turn, which can't be done yet) // specific turn, which can't be done yet)
if (!mandatory && ai.getOpponent().cantLose()) { if (!mandatory && ComputerUtil.getOpponentFor(ai).cantLose()) {
return false; return false;
} }
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(ai.getOpponent()); sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} }
return true; return true;

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -22,7 +23,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) { protected boolean canPlayAI(Player aiPlayer, SpellAbility sa) {
final Random r = MyRandom.getRandom(); final Random r = MyRandom.getRandom();
final int myLife = aiPlayer.getLife(); final int myLife = aiPlayer.getLife();
Player opponent = aiPlayer.getOpponent(); Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
final int hLife = opponent.getLife(); final int hLife = opponent.getLife();
if (!aiPlayer.canGainLife()) { if (!aiPlayer.canGainLife()) {
@@ -78,7 +79,7 @@ public class LifeExchangeAi extends SpellAbilityAi {
final boolean mandatory) { final boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) { if (sa.canTarget(opp) && (mandatory || ai.getLife() < opp.getLife())) {

View File

@@ -1,10 +1,13 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.*; import forge.ai.*;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -35,7 +38,7 @@ public class LifeGainAi extends SpellAbilityAi {
if (!lifeCritical) { if (!lifeCritical) {
// return super.willPayCosts(ai, sa, cost, source); // return super.willPayCosts(ai, sa, cost, source);
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) { if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
return false; return false;
} }
if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) { if (!ComputerUtilCost.checkLifeCost(ai, cost, source, 4, sa)) {
@@ -60,7 +63,7 @@ public class LifeGainAi extends SpellAbilityAi {
skipCheck |= ComputerUtilCost.isSacrificeSelfCost(cost) && !source.isCreature(); skipCheck |= ComputerUtilCost.isSacrificeSelfCost(cost) && !source.isCreature();
if (!skipCheck) { if (!skipCheck) {
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, false)) { if (!ComputerUtilCost.checkSacrificeCost(ai, cost, source, sa,false)) {
return false; return false;
} }
} }
@@ -80,6 +83,23 @@ public class LifeGainAi extends SpellAbilityAi {
lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE) lifeCritical |= ph.getPhase().isBefore(PhaseType.COMBAT_DAMAGE)
&& ComputerUtilCombat.lifeInDanger(ai, game.getCombat()); && ComputerUtilCombat.lifeInDanger(ai, game.getCombat());
// When life is critical but there is no immediate danger, try to wait until declare blockers
// before using the lifegain ability if it's an ability on a creature with a detrimental activation cost
if (lifeCritical
&& sa.isAbility()
&& sa.getHostCard() != null && sa.getHostCard().isCreature()
&& sa.getPayCosts() != null
&& (sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class) || sa.getPayCosts().hasSpecificCostType(CostSacrifice.class))) {
if (!game.getStack().isEmpty()) {
SpellAbility saTop = game.getStack().peekAbility();
if (saTop.getTargets() != null && Iterables.contains(saTop.getTargets().getTargetPlayers(), ai)) {
return ComputerUtil.predictDamageFromSpell(saTop, ai) > 0;
}
}
if (game.getCombat() == null) { return false; }
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS)) { return false; }
}
// Don't use lifegain before main 2 if possible // Don't use lifegain before main 2 if possible
if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases") if (!lifeCritical && ph.getPhase().isBefore(PhaseType.MAIN2) && !sa.hasParam("ActivationPhases")
&& !ComputerUtil.castSpellInMain1(ai, sa)) { && !ComputerUtil.castSpellInMain1(ai, sa)) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -22,7 +23,7 @@ public class LifeSetAi extends SpellAbilityAi {
// Ability_Cost abCost = sa.getPayCosts(); // Ability_Cost abCost = sa.getPayCosts();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final int myLife = ai.getLife(); final int myLife = ai.getLife();
final Player opponent = ai.getOpponent(); final Player opponent = ComputerUtil.getOpponentFor(ai);
final int hlife = opponent.getLife(); final int hlife = opponent.getLife();
final String amountStr = sa.getParam("LifeAmount"); final String amountStr = sa.getParam("LifeAmount");
@@ -93,7 +94,7 @@ public class LifeSetAi extends SpellAbilityAi {
} }
} }
} }
if (amount < myLife) { if (amount <= myLife) {
return false; return false;
} }
} }
@@ -109,7 +110,7 @@ public class LifeSetAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final int myLife = ai.getLife(); final int myLife = ai.getLife();
final Player opponent = ai.getOpponent(); final Player opponent = ComputerUtil.getOpponentFor(ai);
final int hlife = opponent.getLife(); final int hlife = opponent.getLife();
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa); final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);

View File

@@ -12,10 +12,9 @@ import forge.card.ColorSet;
import forge.card.MagicColor; import forge.card.MagicColor;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection; import forge.game.cost.CostPart;
import forge.game.card.CardLists; import forge.game.cost.CostRemoveCounter;
import forge.game.card.CardPredicates;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -110,13 +109,28 @@ public class ManaEffectAi extends SpellAbilityAi {
private boolean doManaRitualLogic(Player ai, SpellAbility sa) { private boolean doManaRitualLogic(Player ai, SpellAbility sa) {
final Card host = sa.getHostCard(); final Card host = sa.getHostCard();
CardCollection manaSources = ComputerUtilMana.getAvailableMana(ai, true); CardCollection manaSources = ComputerUtilMana.getAvailableManaSources(ai, true);
int numManaSrcs = manaSources.size(); int numManaSrcs = manaSources.size();
int manaReceived = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1; int manaReceived = sa.hasParam("Amount") ? AbilityUtils.calculateAmount(host, sa.getParam("Amount"), sa) : 1;
manaReceived *= sa.getParam("Produced").split(" ").length; manaReceived *= sa.getParam("Produced").split(" ").length;
int selfCost = sa.getPayCosts().getCostMana() != null ? sa.getPayCosts().getCostMana().getMana().getCMC() : 0; int selfCost = sa.getPayCosts().getCostMana() != null ? sa.getPayCosts().getCostMana().getMana().getCMC() : 0;
byte producedColor = MagicColor.fromName(sa.getParam("Produced"));
String produced = sa.getParam("Produced");
byte producedColor = produced.equals("Any") ? MagicColor.ALL_COLORS : MagicColor.fromName(produced);
if ("ChosenX".equals(sa.getParam("Amount"))
&& sa.getPayCosts() != null && sa.getPayCosts().hasSpecificCostType(CostRemoveCounter.class)) {
CounterType ctrType = CounterType.KI; // Petalmane Baku
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;
break;
}
}
manaReceived = host.getCounters(ctrType);
}
int searchCMC = numManaSrcs - selfCost + manaReceived; int searchCMC = numManaSrcs - selfCost + manaReceived;
if ("X".equals(sa.getParam("Produced"))) { if ("X".equals(sa.getParam("Produced"))) {

View File

@@ -1,6 +1,8 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -60,7 +62,7 @@ public class MustBlockAi extends SpellAbilityAi {
boolean chance = false; boolean chance = false;
if (abTgt != null) { if (abTgt != null) {
List<Card> list = CardLists.filter(ai.getOpponent().getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES); List<Card> list = CardLists.filter(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), CardPredicates.Presets.CREATURES);
list = CardLists.getTargetableCards(list, sa); list = CardLists.getTargetableCards(list, sa);
list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa); list = CardLists.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {

View File

@@ -62,6 +62,8 @@ public class PermanentAi extends SpellAbilityAi {
return false; return false;
} }
} }
/* -- not used anymore after Ixalan (Planeswalkers are now legendary, not unique by subtype) --
if (card.isPlaneswalker()) { if (card.isPlaneswalker()) {
CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardCollection list = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield),
CardPredicates.Presets.PLANEWALKERS); CardPredicates.Presets.PLANEWALKERS);
@@ -75,7 +77,7 @@ public class PermanentAi extends SpellAbilityAi {
} }
break; break;
} }
} }*/
if (card.getType().hasSupertype(Supertype.World)) { if (card.getType().hasSupertype(Supertype.World)) {
CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World"); CardCollection list = CardLists.getType(ai.getCardsIn(ZoneType.Battlefield), "World");
@@ -175,22 +177,44 @@ public class PermanentAi extends SpellAbilityAi {
if (!hasCard) { if (!hasCard) {
dontCast = true; dontCast = true;
} }
} else if (param.equals("MaxControlled")) { } else if (param.startsWith("MaxControlled")) {
// Only cast unless there are X or more cards like this on the battlefield under AI control already // Only cast unless there are X or more cards like this on the battlefield under AI control already,
int numControlled = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName())).size(); CardCollection ctrld = CardLists.filter(ai.getCardsIn(ZoneType.Battlefield), CardPredicates.nameEquals(card.getName()));
int numControlled = 0;
if (param.endsWith("WithoutOppAuras")) {
// Check that the permanet does not have any auras attached to it by the opponent (this assumes that if
// the opponent cast an aura on the opposing permanent, it's not with good intentions, and thus it might
// be better to have a pristine copy of the card - might not always be a correct assumption, but sounds
// like a reasonable default for some cards).
for (Card c : ctrld) {
if (c.getEnchantedBy(false).isEmpty()) {
numControlled++;
} else {
for (Card att : c.getEnchantedBy(false)) {
if (!att.getController().isOpponentOf(ai)) {
numControlled++;
}
}
}
}
} else {
numControlled = ctrld.size();
}
if (numControlled >= Integer.parseInt(value)) { if (numControlled >= Integer.parseInt(value)) {
dontCast = true; dontCast = true;
} }
} else if (param.equals("NumManaSources")) { } else if (param.equals("NumManaSources")) {
// Only cast if there are X or more mana sources controlled by the AI // Only cast if there are X or more mana sources controlled by the AI
CardCollection m = ComputerUtilMana.getAvailableMana(ai, true); CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
if (m.size() < Integer.parseInt(value)) { if (m.size() < Integer.parseInt(value)) {
dontCast = true; dontCast = true;
} }
} else if (param.equals("NumManaSourcesNextTurn")) { } else if (param.equals("NumManaSourcesNextTurn")) {
// Only cast if there are X or more mana sources controlled by the AI *or* // Only cast if there are X or more mana sources controlled by the AI *or*
// if there are X-1 mana sources in play but the AI has an extra land in hand // if there are X-1 mana sources in play but the AI has an extra land in hand
CardCollection m = ComputerUtilMana.getAvailableMana(ai, true); CardCollection m = ComputerUtilMana.getAvailableManaSources(ai, true);
int extraMana = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size() > 0 ? 1 : 0; int extraMana = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size() > 0 ? 1 : 0;
if (card.getName().equals("Illusions of Grandeur")) { if (card.getName().equals("Illusions of Grandeur")) {
// TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic. // TODO: this is currently hardcoded for specific Illusions-Donate cost reduction spells, need to make this generic.

View File

@@ -99,7 +99,8 @@ public class PlayAi extends SpellAbilityAi {
* @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean) * @see forge.card.ability.SpellAbilityAi#chooseSingleCard(forge.game.player.Player, forge.card.spellability.SpellAbility, java.util.List, boolean)
*/ */
@Override @Override
public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options, boolean isOptional, public Card chooseSingleCard(final Player ai, final SpellAbility sa, Iterable<Card> options,
final boolean isOptional,
Player targetedPlayer) { Player targetedPlayer) {
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() { List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
@Override @Override

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
@@ -31,7 +32,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
sa.resetTargets(); sa.resetTargets();
List<Card> list = List<Card> list =
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa); CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), tgt.getValidTgts(), ai, sa.getHostCard(), sa);
// AI won't try to grab cards that are filtered out of AI decks on // AI won't try to grab cards that are filtered out of AI decks on
// purpose // purpose
list = CardLists.filter(list, new Predicate<Card>() { list = CardLists.filter(list, new Predicate<Card>() {

View File

@@ -147,9 +147,10 @@ public class ProtectAi extends SpellAbilityAi {
if (s==null) { if (s==null) {
return false; return false;
} else { } else {
Player opponent = ComputerUtil.getOpponentFor(ai);
Combat combat = ai.getGame().getCombat(); Combat combat = ai.getGame().getCombat();
int dmg = ComputerUtilCombat.damageIfUnblocked(c, ai.getOpponent(), combat, true); int dmg = ComputerUtilCombat.damageIfUnblocked(c, opponent, combat, true);
float ratio = 1.0f * dmg / ai.getOpponent().getLife(); float ratio = 1.0f * dmg / opponent.getLife();
Random r = MyRandom.getRandom(); Random r = MyRandom.getRandom();
return r.nextFloat() < ratio; return r.nextFloat() < ratio;
} }

View File

@@ -29,7 +29,7 @@ public class ProtectAllAi extends SpellAbilityAi {
return false; return false;
} }
if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard)) { if (!ComputerUtilCost.checkSacrificeCost(ai, cost, hostCard, sa)) {
return false; return false;
} }

View File

@@ -1,31 +1,18 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.Arrays;
import java.util.List;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Lists; import com.google.common.collect.Lists;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.card.CardUtil; import forge.game.combat.Combat;
import forge.game.card.CounterType;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostTapType; import forge.game.cost.CostTapType;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
@@ -35,6 +22,11 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityRestriction; import forge.game.spellability.SpellAbilityRestriction;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import org.apache.commons.lang3.StringUtils;
import java.util.Arrays;
import java.util.List;
public class PumpAi extends PumpAiBase { public class PumpAi extends PumpAiBase {
@@ -82,16 +74,17 @@ public class PumpAi extends PumpAiBase {
System.err.println("MoveCounter AiLogic without MoveCounter SubAbility!"); System.err.println("MoveCounter AiLogic without MoveCounter SubAbility!");
return false; return false;
} }
} else if ("Aristocrat".equals(aiLogic)) {
return doAristocratLogic(sa, ai);
} }
return super.checkAiLogic(ai, sa, aiLogic); return super.checkAiLogic(ai, sa, aiLogic);
} }
@Override @Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph, protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph,
final String logic) { final String logic) {
// special Phase check for MoveCounter // special Phase check for various AI logics
if (logic.equals("MoveCounter")) { if (logic.equals("MoveCounter")) {
if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) { if (ph.inCombat() && ph.getPlayerTurn().isOpponentOf(ai)) {
return true; return true;
@@ -101,6 +94,11 @@ public class PumpAi extends PumpAiBase {
return false; return false;
} }
return true; return true;
} else if (logic.equals("Aristocrat")) {
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(sa.getHostCard());
if (!ph.is(PhaseType.COMBAT_DECLARE_BLOCKERS) && !isThreatened) {
return false;
}
} }
return super.checkPhaseRestrictions(ai, sa, ph); return super.checkPhaseRestrictions(ai, sa, ph);
} }
@@ -112,7 +110,7 @@ public class PumpAi extends PumpAiBase {
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) { if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_ATTACKERS) && ph.isPlayerTurn(ai)) {
return false; return false;
} }
if (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS) && ph.getPlayerTurn().isOpponentOf(ai)) { if (ph.getPhase().isBefore(PhaseType.COMBAT_BEGIN) && ph.getPlayerTurn().isOpponentOf(ai)) {
return false; return false;
} }
} }
@@ -141,7 +139,9 @@ public class PumpAi extends PumpAiBase {
final boolean isFight = "Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic); final boolean isFight = "Fight".equals(aiLogic) || "PowerDmg".equals(aiLogic);
final boolean isBerserk = "Berserk".equals(aiLogic); final boolean isBerserk = "Berserk".equals(aiLogic);
if ("MoveCounter".equals(aiLogic)) { if ("Pummeler".equals(aiLogic)) {
return SpecialCardAi.ElectrostaticPummeler.consider(ai, sa);
} else if ("MoveCounter".equals(aiLogic)) {
final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter); final SpellAbility moveSA = sa.findSubAbilityByType(ApiType.MoveCounter);
if (moveSA == null) { if (moveSA == null) {
@@ -359,9 +359,12 @@ public class PumpAi extends PumpAiBase {
return true; return true;
} }
if (!card.getController().isOpponentOf(ai) if (!card.getController().isOpponentOf(ai)) {
&& ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) { if (ComputerUtilCard.shouldPumpCard(ai, sa, card, defense, attack, keywords, false)) {
return true; return true;
} else if (containsUsefulKeyword(ai, keywords, card, sa, attack)) {
return true;
}
} }
} }
return false; return false;
@@ -371,6 +374,21 @@ public class PumpAi extends PumpAiBase {
return false; return false;
} }
if ("DebuffForXCounters".equals(sa.getParam("AILogic")) && sa.getTargetCard() != null) {
// e.g. Skullmane Baku
CounterType ctrType = CounterType.KI;
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;
break;
}
}
// Do not pay more counters than necessary to kill the targeted creature
int chosenX = Math.min(source.getCounters(ctrType), sa.getTargetCard().getNetToughness());
sa.setSVar("ChosenX", String.valueOf(chosenX));
}
return true; return true;
} // pumpPlayAI() } // pumpPlayAI()
@@ -461,6 +479,22 @@ public class PumpAi extends PumpAiBase {
} }
} }
// Detain target nonland permanent: don't target noncreature permanents that don't have
// any activated abilities.
if ("DetainNonLand".equals(sa.getParam("AILogic"))) {
list = CardLists.filter(list, Predicates.or(CardPredicates.Presets.CREATURES, new Predicate<Card>() {
@Override
public boolean apply(Card card) {
for (SpellAbility sa: card.getSpellAbilities()) {
if (sa.isAbility()) {
return true;
}
}
return false;
}
}));
}
if (list.isEmpty()) { if (list.isEmpty()) {
if (ComputerUtil.activateForCost(sa, ai)) { if (ComputerUtil.activateForCost(sa, ai)) {
return pumpMandatoryTarget(ai, sa); return pumpMandatoryTarget(ai, sa);
@@ -695,8 +729,6 @@ public class PumpAi extends PumpAiBase {
return true; return true;
} // pumpDrawbackAI() } // pumpDrawbackAI()
@Override @Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) { public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
//TODO Add logic here if necessary but I think the AI won't cast //TODO Add logic here if necessary but I think the AI won't cast
@@ -704,4 +736,123 @@ public class PumpAi extends PumpAiBase {
//and the pump isn't mandatory //and the pump isn't mandatory
return true; return true;
} }
public boolean doAristocratLogic(final SpellAbility sa, final Player ai) {
// A logic for cards that say "Sacrifice a creature: CARDNAME gets +X/+X until EOT"
final Game game = ai.getGame();
final Combat combat = game.getCombat();
final Card source = sa.getHostCard();
final int numOtherCreats = Math.max(0, ai.getCreaturesInPlay().size() - 1);
final int powerBonus = sa.hasParam("NumAtt") ? AbilityUtils.calculateAmount(source, sa.getParam("NumAtt"), sa) : 0;
final int toughnessBonus = sa.hasParam("NumDef") ? AbilityUtils.calculateAmount(source, sa.getParam("NumDef"), sa) : 0;
final int selfEval = ComputerUtilCard.evaluateCreature(source);
final boolean isThreatened = ComputerUtil.predictThreatenedObjects(ai, null, true).contains(source);
if (numOtherCreats == 0) {
return false;
}
// Try to save the card from death by pumping it if it's threatened with a damage spell
if (isThreatened && toughnessBonus > 0) {
SpellAbility saTop = game.getStack().peekAbility();
if (saTop.getApi() == ApiType.DealDamage || saTop.getApi() == ApiType.DamageAll) {
int dmg = AbilityUtils.calculateAmount(saTop.getHostCard(), saTop.getParam("NumDmg"), saTop) + source.getDamage();
final int numCreatsToSac = Math.max(1, (int)Math.ceil((dmg - source.getNetToughness() + 1) / toughnessBonus));
if (numCreatsToSac > 1) { // probably not worth sacrificing too much
return false;
}
if (source.getNetToughness() <= dmg && source.getNetToughness() + toughnessBonus * numCreatsToSac > dmg) {
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| card.hasSVar("SacMe")
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
}
}
);
if (sacFodder.size() >= numCreatsToSac) {
return true;
}
}
}
return false;
}
if (combat == null) {
return false;
}
if (combat.isAttacking(source)) {
if (combat.getBlockers(source).isEmpty()) {
// Unblocked. Check if able to deal lethal, then sac'ing everything is fair game if
// the opponent is tapped out or if we're willing to risk it (will currently risk it
// in case it sacs less than half its creatures to deal lethal damage)
// TODO: also teach the AI to account for Trample, but that's trickier (needs to account fully
// for potential damage prevention, various effects like reducing damage to 0, etc.)
final Player defPlayer = combat.getDefendingPlayerRelatedTo(source);
final boolean defTappedOut = ComputerUtilMana.getAvailableManaEstimate(defPlayer) == 0;
final boolean isInfect = source.hasKeyword("Infect"); // Flesh-Eater Imp
int lethalDmg = isInfect ? 10 - defPlayer.getPoisonCounters() : defPlayer.getLife();
if (isInfect && !combat.getDefenderByAttacker(source).canReceiveCounters(CounterType.POISON)) {
lethalDmg = Integer.MAX_VALUE; // won't be able to deal poison damage to kill the opponent
}
final int numCreatsToSac = (lethalDmg - source.getNetCombatDamage()) / powerBonus;
if (defTappedOut || numCreatsToSac < numOtherCreats / 2) {
return source.getNetCombatDamage() < lethalDmg
&& source.getNetCombatDamage() + numOtherCreats * powerBonus >= lethalDmg;
} else {
return false;
}
} else {
// We have already attacked. Thus, see if we have a creature to sac that is worse to lose
// than the card we attacked with.
final CardCollection sacTgts = CardLists.filter(ai.getCreaturesInPlay(),
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| ComputerUtilCard.evaluateCreature(card) < selfEval;
}
}
);
if (sacTgts.isEmpty()) {
return false;
}
final int minDefT = Aggregates.min(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetToughness);
final int DefP = Aggregates.sum(combat.getBlockers(source), CardPredicates.Accessors.fnGetNetPower);
// Make sure we don't over-sacrifice, only sac until we can survive and kill a creature
return source.getNetToughness() - source.getDamage() <= DefP || source.getNetCombatDamage() < minDefT;
}
} else {
// We can't deal lethal, check if there's any sac fodder than can be used for other circumstances
final CardCollection sacFodder = CardLists.filter(ai.getCreaturesInPlay(),
new Predicate<Card>() {
@Override
public boolean apply(Card card) {
return ComputerUtilCard.isUselessCreature(ai, card)
|| card.hasSVar("SacMe")
|| ComputerUtilCard.evaluateCreature(card) < selfEval; // Maybe around 150 is OK?
}
}
);
return !sacFodder.isEmpty();
}
}
} }

View File

@@ -181,7 +181,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
final Game game = ai.getGame(); final Game game = ai.getGame();
final Combat combat = game.getCombat(); final Combat combat = game.getCombat();
final PhaseHandler ph = game.getPhaseHandler(); final PhaseHandler ph = game.getPhaseHandler();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final int newPower = card.getNetCombatDamage() + attack; final int newPower = card.getNetCombatDamage() + attack;
//int defense = getNumDefense(sa); //int defense = getNumDefense(sa);
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) { if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {
@@ -207,6 +207,19 @@ public abstract class PumpAiBase extends SpellAbilityAi {
return true; return true;
} }
Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach")); Predicate<Card> flyingOrReach = Predicates.or(CardPredicates.hasKeyword("Flying"), CardPredicates.hasKeyword("Reach"));
if (ph.isPlayerTurn(opp) && combat != null
&& Iterables.any(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))
&& CombatUtil.canBlock(card)) {
// Use defensively to destroy the opposing Flying creature when possible, or to block with an indestructible
// creature buffed with Flying
for (Card c : CardLists.filter(combat.getAttackers(), CardPredicates.hasKeyword("Flying"))) {
if (!ComputerUtilCombat.attackerCantBeDestroyedInCombat(c.getController(), c)
&& (card.getNetPower() >= c.getNetToughness() && card.getNetToughness() > c.getNetPower()
|| ComputerUtilCombat.attackerCantBeDestroyedInCombat(ai, card))) {
return true;
}
}
}
if (ph.isPlayerTurn(opp) || !(CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card))) if (ph.isPlayerTurn(opp) || !(CombatUtil.canAttack(card, opp) || (combat != null && combat.isAttacking(card)))
|| ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS) || ph.getPhase().isAfter(PhaseType.COMBAT_DECLARE_ATTACKERS)
|| newPower <= 0 || newPower <= 0
@@ -371,7 +384,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
|| !CombatUtil.canBlock(card)) { || !CombatUtil.canBlock(card)) {
return false; return false;
} }
} else if (keyword.endsWith("CARDNAME can block an additional creature.")) { } else if (keyword.endsWith("CARDNAME can block an additional creature each combat.")) {
if (ph.isPlayerTurn(ai) if (ph.isPlayerTurn(ai)
|| !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) { || !ph.getPhase().equals(PhaseType.COMBAT_DECLARE_ATTACKERS)) {
return false; return false;

View File

@@ -48,7 +48,7 @@ public class PumpAllAi extends PumpAiBase {
} }
if (abCost != null && source.hasSVar("AIPreference")) { if (abCost != null && source.hasSVar("AIPreference")) {
if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, true)) { if (!ComputerUtilCost.checkSacrificeCost(ai, abCost, source, sa, true)) {
return false; return false;
} }
} }
@@ -57,7 +57,7 @@ public class PumpAllAi extends PumpAiBase {
valid = sa.getParam("ValidCards"); valid = sa.getParam("ValidCards");
} }
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source); CardCollection comp = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);
CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source); CardCollection human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);

View File

@@ -1,6 +1,7 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -27,7 +28,7 @@ public class RearrangeTopOfLibraryAi extends SpellAbilityAi {
// ability is targeted // ability is targeted
sa.resetTargets(); sa.resetTargets();
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
final boolean canTgtHuman = opp.canBeTargetedBy(sa); final boolean canTgtHuman = opp.canBeTargetedBy(sa);
if (!canTgtHuman) { if (!canTgtHuman) {

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import forge.ai.AiController; import forge.ai.AiController;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.PlayerControllerAi; import forge.ai.PlayerControllerAi;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -17,7 +18,7 @@ public class RepeatAi extends SpellAbilityAi {
protected boolean canPlayAI(Player ai, SpellAbility sa) { protected boolean canPlayAI(Player ai, SpellAbility sa) {
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
if (tgt != null) { if (tgt != null) {
if (!opp.canBeTargetedBy(sa)) { if (!opp.canBeTargetedBy(sa)) {
@@ -48,7 +49,7 @@ public class RepeatAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { if (sa.usesTargeting()) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.canTarget(opp)) { if (sa.canTarget(opp)) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(opp); sa.getTargets().add(opp);

View File

@@ -15,6 +15,7 @@ import forge.game.player.Player;
import forge.game.spellability.AbilitySub; import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.TextUtil;
public class RepeatEachAi extends SpellAbilityAi { public class RepeatEachAi extends SpellAbilityAi {
@@ -85,7 +86,7 @@ public class RepeatEachAi extends SpellAbilityAi {
String svar = repeat.getSVar(repeat.getParam("LifeAmount")); String svar = repeat.getSVar(repeat.getParam("LifeAmount"));
// replace RememberedPlayerCtrl with YouCtrl // replace RememberedPlayerCtrl with YouCtrl
String svarYou = svar.replace("RememberedPlayer", "You"); String svarYou = TextUtil.fastReplace(svar, "RememberedPlayer", "You");
// Currently all Cards with that are affect all player, including AI // Currently all Cards with that are affect all player, including AI
if (aiPlayer.canLoseLife()) { if (aiPlayer.canLoseLife()) {

View File

@@ -11,6 +11,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil;
public class RollPlanarDiceAi extends SpellAbilityAi { public class RollPlanarDiceAi extends SpellAbilityAi {
/* (non-Javadoc) /* (non-Javadoc)
@@ -107,7 +108,7 @@ public class RollPlanarDiceAi extends SpellAbilityAi {
} }
break; break;
default: default:
System.out.println(String.format("Unexpected AI hint parameter in card %s in RollPlanarDiceAi: %s.", plane.getName(), paramName)); System.out.println(TextUtil.concatNoSpace("Unexpected AI hint parameter in card ", plane.getName(), " in RollPlanarDiceAi: ", paramName, "."));
break; break;
} }
} }

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -59,7 +60,7 @@ public class SacrificeAi extends SpellAbilityAi {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
final boolean destroy = sa.hasParam("Destroy"); final boolean destroy = sa.hasParam("Destroy");
Player opp = ai.getOpponent(); Player opp = ComputerUtil.getOpponentFor(ai);
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (!opp.canBeTargetedBy(sa)) { if (!opp.canBeTargetedBy(sa)) {
@@ -72,7 +73,7 @@ public class SacrificeAi extends SpellAbilityAi {
final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa); final int amount = AbilityUtils.calculateAmount(sa.getHostCard(), num, sa);
List<Card> list = List<Card> list =
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa); CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid.split(","), sa.getActivatingPlayer(), sa.getHostCard(), sa);
for (Card c : list) { for (Card c : list) {
if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) { if (c.hasSVar("SacMe") && Integer.parseInt(c.getSVar("SacMe")) > 3) {
return false; return false;

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
@@ -12,6 +13,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil;
import java.util.Random; import java.util.Random;
@@ -34,11 +36,11 @@ public class SacrificeAllAi extends SpellAbilityAi {
// Set PayX here to maximum value. // Set PayX here to maximum value.
final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai); final int xPay = ComputerUtilMana.determineLeftoverMana(sa, ai);
source.setSVar("PayX", Integer.toString(xPay)); source.setSVar("PayX", Integer.toString(xPay));
valid = valid.replace("X", Integer.toString(xPay)); valid = TextUtil.fastReplace(valid, "X", Integer.toString(xPay));
} }
CardCollection humanlist = CardCollection humanlist =
CardLists.getValidCards(ai.getOpponent().getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); CardLists.getValidCards(ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);
CardCollection computerlist = CardCollection computerlist =
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa); CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);

View File

@@ -1,14 +1,11 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicates;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.Card.SplitCMCMode; import forge.game.card.Card.SplitCMCMode;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode; import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
@@ -18,8 +15,6 @@ import forge.util.MyRandom;
import java.util.Random; import java.util.Random;
import com.google.common.base.Predicates;
public class ScryAi extends SpellAbilityAi { public class ScryAi extends SpellAbilityAi {
/* (non-Javadoc) /* (non-Javadoc)
@@ -49,6 +44,18 @@ public class ScryAi extends SpellAbilityAi {
*/ */
@Override @Override
protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) { protected boolean checkPhaseRestrictions(final Player ai, final SpellAbility sa, final PhaseHandler ph) {
// if the Scry ability requires tapping and has a mana cost, it's best done at the end of opponent's turn
// and right before the beginning of AI's turn, if possible, to avoid mana locking the AI and also to
// try to scry right before drawing a card. Also, avoid tapping creatures in the AI's turn, if possible,
// even if there's no mana cost.
if (sa.getPayCosts() != null) {
if (sa.getPayCosts().hasTapCost()
&& (sa.getPayCosts().hasManaCost() || (sa.getHostCard() != null && sa.getHostCard().isCreature()))
&& !SpellAbilityAi.isSorcerySpeed(sa)) {
return ph.getNextTurn() == ai && ph.is(PhaseType.END_OF_TURN);
}
}
// in the playerturn Scry should only be done in Main1 or in upkeep if able // in the playerturn Scry should only be done in Main1 or in upkeep if able
if (ph.isPlayerTurn(ai)) { if (ph.isPlayerTurn(ai)) {
if (SpellAbilityAi.isSorcerySpeed(sa)) { if (SpellAbilityAi.isSorcerySpeed(sa)) {

View File

@@ -1,23 +1,19 @@
package forge.ai.ability; package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.card.CardSplitType; import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.game.Game; import forge.game.Game;
import forge.game.GlobalRuleChange; import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardState;
import forge.game.card.CardUtil;
import forge.game.card.CounterType;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
@@ -67,6 +63,13 @@ public class SetStateAi extends SpellAbilityAi {
return false; return false;
} }
@Override
protected boolean checkAiLogic(final Player aiPlayer, final SpellAbility sa, final String aiLogic) {
final Card source = sa.getHostCard();
return super.checkAiLogic(aiPlayer, sa, aiLogic);
}
@Override @Override
public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) { public boolean chkAIDrawback(SpellAbility sa, Player aiPlayer) {
// Gross generalization, but this always considers alternate // Gross generalization, but this always considers alternate
@@ -78,6 +81,7 @@ public class SetStateAi extends SpellAbilityAi {
protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) { protected boolean checkPhaseRestrictions(Player ai, SpellAbility sa, PhaseHandler ph) {
final String mode = sa.getParam("Mode"); final String mode = sa.getParam("Mode");
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final String logic = sa.getParamOrDefault("AILogic", "");
final Game game = source.getGame(); final Game game = source.getGame();
if("Transform".equals(mode)) { if("Transform".equals(mode)) {
@@ -86,7 +90,7 @@ public class SetStateAi extends SpellAbilityAi {
if (source.hasKeyword("CARDNAME can't transform")) { if (source.hasKeyword("CARDNAME can't transform")) {
return false; return false;
} }
return shouldTransformCard(source, ai, ph); return shouldTransformCard(source, ai, ph) || "Always".equals(logic);
} else { } else {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
sa.resetTargets(); sa.resetTargets();
@@ -108,7 +112,7 @@ public class SetStateAi extends SpellAbilityAi {
} }
for (final Card c : list) { for (final Card c : list) {
if (shouldTransformCard(c, ai, ph)) { if (shouldTransformCard(c, ai, ph) || "Always".equals(logic)) {
sa.getTargets().add(c); sa.getTargets().add(c);
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) { if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
break; break;
@@ -126,7 +130,7 @@ public class SetStateAi extends SpellAbilityAi {
if (list.isEmpty()) { if (list.isEmpty()) {
return false; return false;
} }
return shouldTurnFace(list.get(0), ai, ph); return shouldTurnFace(list.get(0), ai, ph) || "Always".equals(logic);
} else { } else {
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
sa.resetTargets(); sa.resetTargets();
@@ -139,7 +143,7 @@ public class SetStateAi extends SpellAbilityAi {
} }
for (final Card c : list) { for (final Card c : list) {
if (shouldTurnFace(c, ai, ph)) { if (shouldTurnFace(c, ai, ph) || "Always".equals(logic)) {
sa.getTargets().add(c); sa.getTargets().add(c);
if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) { if (sa.getTargets().getNumTargeted() == tgt.getMaxTargets(source, sa)) {
break; break;
@@ -166,12 +170,26 @@ public class SetStateAi extends SpellAbilityAi {
transformed.getCurrentState().copyFrom(card, card.getAlternateState()); transformed.getCurrentState().copyFrom(card, card.getAlternateState());
transformed.updateStateForView(); transformed.updateStateForView();
// TODO: compareCards assumes that a creature will transform into a creature. Need to improve this
// for other things potentially transforming.
return compareCards(card, transformed, ai, ph); return compareCards(card, transformed, ai, ph);
} }
private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) { private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
if (card.isFaceDown()) { if (card.isFaceDown()) {
// hidden agenda
if (card.getState(CardStateName.Original).hasIntrinsicKeyword("Hidden agenda")
&& card.getZone().is(ZoneType.Command)) {
String chosenName = card.getNamedCard();
for (Card cast : ai.getGame().getStack().getSpellsCastThisTurn()) {
if (cast.getController() == ai && cast.getName().equals(chosenName)) {
return true;
}
}
return false;
}
// non-permanent facedown can't be turned face up // non-permanent facedown can't be turned face up
if (!card.getRules().getType().isPermanent()) { if (!card.getRules().getType().isPermanent()) {
return false; return false;
@@ -241,4 +259,9 @@ public class SetStateAi extends SpellAbilityAi {
// but for more cleaner way use Evaluate for check // but for more cleaner way use Evaluate for check
return valueCard <= valueTransformed; return valueCard <= valueTransformed;
} }
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
// TODO: improve the AI for when it may want to transform something that's optional to transform
return true;
}
} }

View File

@@ -5,7 +5,10 @@ import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterType;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.phase.PhaseHandler; import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -58,6 +61,20 @@ public class TapAi extends TapAiBase {
return false; return false;
} }
} else { } else {
if ("TapForXCounters".equals(sa.getParam("AILogic"))) {
// e.g. Waxmane Baku
CounterType ctrType = CounterType.KI;
for (CostPart part : sa.getPayCosts().getCostParts()) {
if (part instanceof CostRemoveCounter) {
ctrType = ((CostRemoveCounter)part).counter;
break;
}
}
int numTargetable = Math.min(sa.getHostCard().getCounters(ctrType), ai.getOpponents().getCreaturesInPlay().size());
sa.setSVar("ChosenX", String.valueOf(numTargetable));
}
sa.resetTargets(); sa.resetTargets();
if (!tapPrefTargeting(ai, source, tgt, sa, false)) { if (!tapPrefTargeting(ai, source, tgt, sa, false)) {
return false; return false;

View File

@@ -111,7 +111,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
* @return a boolean. * @return a boolean.
*/ */
protected boolean tapPrefTargeting(final Player ai, final Card source, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) { protected boolean tapPrefTargeting(final Player ai, final Card source, final TargetRestrictions tgt, final SpellAbility sa, final boolean mandatory) {
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final Game game = ai.getGame(); final Game game = ai.getGame();
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents()); CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa); tapList = CardLists.getValidCards(tapList, tgt.getValidTgts(), source.getController(), source, sa);

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat; import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.Game; import forge.game.Game;
@@ -30,7 +31,7 @@ public class TapAllAi extends SpellAbilityAi {
// or during upkeep/begin combat? // or during upkeep/begin combat?
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final Game game = ai.getGame(); final Game game = ai.getGame();
if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) { if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
@@ -126,8 +127,8 @@ public class TapAllAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
sa.getTargets().add(ai.getOpponent()); sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
validTappables = ai.getOpponent().getCardsIn(ZoneType.Battlefield); validTappables = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
} }
if (mandatory) { if (mandatory) {

View File

@@ -1,27 +1,14 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import com.google.common.base.Predicate; import com.google.common.base.Predicate;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import forge.ai.*;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.ai.SpellApiToAi;
import forge.game.Game; import forge.game.Game;
import forge.game.GameEntity; import forge.game.GameEntity;
import forge.game.ability.AbilityFactory; import forge.game.ability.AbilityFactory;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType; import forge.game.ability.ApiType;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardFactory;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.combat.Combat; import forge.game.combat.Combat;
import forge.game.cost.CostPart; import forge.game.cost.CostPart;
import forge.game.cost.CostPutCounter; import forge.game.cost.CostPutCounter;
@@ -38,6 +25,11 @@ import forge.game.trigger.TriggerHandler;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.item.PaperToken; import forge.item.PaperToken;
import forge.util.MyRandom; import forge.util.MyRandom;
import forge.util.TextUtil;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/** /**
* <p> * <p>
@@ -179,7 +171,7 @@ public class TokenAi extends SpellAbilityAi {
*/ */
final Card source = sa.getHostCard(); final Card source = sa.getHostCard();
final Game game = ai.getGame(); final Game game = ai.getGame();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
if (ComputerUtil.preventRunAwayActivations(sa)) { if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite tokens? return false; // prevent infinite tokens?
@@ -233,7 +225,29 @@ public class TokenAi extends SpellAbilityAi {
} }
} }
} }
return MyRandom.getRandom().nextFloat() < .8;
double chance = 1.0F; // 100%
boolean alwaysFromPW = true;
boolean alwaysOnOppAttack = true;
if (ai.getController().isAI()) {
AiController aic = ((PlayerControllerAi) ai.getController()).getAi();
chance = (double)aic.getIntProperty(AiProps.TOKEN_GENERATION_ABILITY_CHANCE) / 100;
alwaysFromPW = aic.getBooleanProperty(AiProps.TOKEN_GENERATION_ALWAYS_IF_FROM_PLANESWALKER);
alwaysOnOppAttack = aic.getBooleanProperty(AiProps.TOKEN_GENERATION_ALWAYS_IF_OPP_ATTACKS);
}
if (sa.getRestrictions() != null && sa.getRestrictions().isPwAbility() && alwaysFromPW) {
return true;
} else if (ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_ATTACKERS)
&& ai.getGame().getPhaseHandler().getPlayerTurn().isOpponentOf(ai)
&& ai.getGame().getCombat() != null
&& !ai.getGame().getCombat().getAttackers().isEmpty()
&& alwaysOnOppAttack) {
return true;
}
return MyRandom.getRandom().nextFloat() <= chance;
} }
/** /**
@@ -254,13 +268,13 @@ public class TokenAi extends SpellAbilityAi {
num = (num == null) ? "1" : num; num = (num == null) ? "1" : num;
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack); final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), CardCollection list = CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","),
ai.getOpponent(), topStack.getHostCard(), sa); ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), sa);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
// only care about saving single creature for now // only care about saving single creature for now
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) { if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
ComputerUtilCard.sortByEvaluateCreature(list); ComputerUtilCard.sortByEvaluateCreature(list);
list.add(token); list.add(token);
list = CardLists.getValidCards(list, valid.split(","), ai.getOpponent(), topStack.getHostCard(), sa); list = CardLists.getValidCards(list, valid.split(","), ComputerUtil.getOpponentFor(ai), topStack.getHostCard(), sa);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack)); list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
if (ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0)) if (ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
&& list.contains(token)) { && list.contains(token)) {
@@ -278,7 +292,7 @@ public class TokenAi extends SpellAbilityAi {
if (tgt != null) { if (tgt != null) {
sa.resetTargets(); sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) { if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(ai.getOpponent()); sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} else { } else {
sa.getTargets().add(ai); sa.getTargets().add(ai);
} }
@@ -414,7 +428,7 @@ public class TokenAi extends SpellAbilityAi {
final List<String> imageNames = new ArrayList<String>(1); final List<String> imageNames = new ArrayList<String>(1);
if (tokenImage.equals("")) { if (tokenImage.equals("")) {
imageNames.add(PaperToken.makeTokenFileName(colorDesc.replace(" ", ""), tokenPower, tokenToughness, tokenName)); imageNames.add(PaperToken.makeTokenFileName(TextUtil.fastReplace(colorDesc, " ", ""), tokenPower, tokenToughness, tokenName));
} else { } else {
imageNames.add(0, tokenImage); imageNames.add(0, tokenImage);
} }

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.List; import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils; import forge.game.ability.AbilityUtils;
import forge.game.card.Card; import forge.game.card.Card;
@@ -28,7 +29,7 @@ public class TwoPilesAi extends SpellAbilityAi {
valid = sa.getParam("ValidCards"); valid = sa.getParam("ValidCards");
} }
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { if (tgt != null) {

View File

@@ -1,5 +1,6 @@
package forge.ai.ability; package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilMana; import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi; import forge.ai.SpellAbilityAi;
@@ -66,7 +67,7 @@ public class UnattachAllAi extends SpellAbilityAi {
@Override @Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) { protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final Card card = sa.getHostCard(); final Card card = sa.getHostCard();
final Player opp = ai.getOpponent(); final Player opp = ComputerUtil.getOpponentFor(ai);
// Check if there are any valid targets // Check if there are any valid targets
List<GameObject> targets = new ArrayList<GameObject>(); List<GameObject> targets = new ArrayList<GameObject>();
final TargetRestrictions tgt = sa.getTargetRestrictions(); final TargetRestrictions tgt = sa.getTargetRestrictions();

View File

@@ -1,7 +1,5 @@
package forge.ai.ability; package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard; import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
@@ -13,6 +11,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists; import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets; import forge.game.card.CardPredicates.Presets;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.cost.CostTap;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.player.PlayerCollection; import forge.game.player.PlayerCollection;
@@ -20,6 +19,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import java.util.List;
public class UntapAi extends SpellAbilityAi { public class UntapAi extends SpellAbilityAi {
@Override @Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) { protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
@@ -132,7 +133,7 @@ public class UntapAi extends SpellAbilityAi {
Player targetController = ai; Player targetController = ai;
if (sa.isCurse()) { if (sa.isCurse()) {
targetController = ai.getOpponent(); targetController = ComputerUtil.getOpponentFor(ai);
} }
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa); CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
@@ -148,6 +149,24 @@ public class UntapAi extends SpellAbilityAi {
final String[] tappablePermanents = {"Creature", "Land", "Artifact"}; final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa); untapList = CardLists.getValidCards(untapList, tappablePermanents, source.getController(), source, sa);
// Try to avoid potential infinite recursion,
// e.g. Kiora's Follower untapping another Kiora's Follower and repeating infinitely
if (sa.getPayCosts() != null && sa.getPayCosts().hasOnlySpecificCostType(CostTap.class)) {
CardCollection toRemove = new CardCollection();
for (Card c : untapList) {
for (SpellAbility ab : c.getAllSpellAbilities()) {
if (ab.getApi() == ApiType.Untap
&& ab.getPayCosts() != null
&& ab.getPayCosts().hasOnlySpecificCostType(CostTap.class)
&& ab.canTarget(source)) {
toRemove.add(c);
break;
}
}
}
untapList.removeAll(toRemove);
}
sa.resetTargets(); sa.resetTargets();
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) { while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
Card choice = null; Card choice = null;

View File

@@ -303,7 +303,7 @@ public class GameCopier {
newCard.setSemiPermanentToughnessBoost(c.getSemiPermanentToughnessBoost()); newCard.setSemiPermanentToughnessBoost(c.getSemiPermanentToughnessBoost());
newCard.setDamage(c.getDamage()); newCard.setDamage(c.getDamage());
newCard.setChangedCardTypes(c.getChangedCardTypes()); newCard.setChangedCardTypes(c.getChangedCardTypesMap());
newCard.setChangedCardKeywords(c.getChangedCardKeywords()); newCard.setChangedCardKeywords(c.getChangedCardKeywords());
// TODO: Is this correct? Does it not duplicate keywords from enchantments and such? // TODO: Is this correct? Does it not duplicate keywords from enchantments and such?
for (String kw : c.getHiddenExtrinsicKeywords()) for (String kw : c.getHiddenExtrinsicKeywords())

View File

@@ -17,6 +17,7 @@ import forge.game.player.Player;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetChoices; import forge.game.spellability.TargetChoices;
import forge.game.spellability.TargetRestrictions; import forge.game.spellability.TargetRestrictions;
import forge.util.TextUtil;
public class GameSimulator { public class GameSimulator {
public static boolean COPY_STACK = false; public static boolean COPY_STACK = false;
@@ -136,7 +137,7 @@ public class GameSimulator {
System.err.println(sa.getDescription() + "->" + desc); System.err.println(sa.getDescription() + "->" + desc);
} }
// FIXME: This is a hack that makes testManifest pass - figure out why it's needed. // FIXME: This is a hack that makes testManifest pass - figure out why it's needed.
desc = desc.replace("Unmanifest {0}", "Unmanifest no cost"); desc = TextUtil.fastReplace(desc, "Unmanifest {0}", "Unmanifest no cost");
for (SpellAbility cSa : hostCard.getSpellAbilities()) { for (SpellAbility cSa : hostCard.getSpellAbilities()) {
if (desc.equals(cSa.getDescription())) { if (desc.equals(cSa.getDescription())) {
return cSa; return cSa;
@@ -204,7 +205,7 @@ public class GameSimulator {
} }
// TODO: Support multiple opponents. // TODO: Support multiple opponents.
Player opponent = aiPlayer.getOpponent(); Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
resolveStack(simGame, opponent); resolveStack(simGame, opponent);
// TODO: If this is during combat, before blockers are declared, // TODO: If this is during combat, before blockers are declared,

View File

@@ -4,7 +4,6 @@ import forge.ai.CreatureEvaluator;
import forge.game.Game; import forge.game.Game;
import forge.game.card.Card; import forge.game.card.Card;
import forge.game.card.CounterType; import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;

View File

@@ -1,22 +1,16 @@
package forge.ai.simulation; package forge.ai.simulation;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import forge.ai.AiPlayDecision; import forge.ai.AiPlayDecision;
import forge.ai.ComputerUtil; import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility; import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCost; import forge.ai.ComputerUtilCost;
import forge.ai.ability.ChangeZoneAi; import forge.ai.ability.ChangeZoneAi;
import forge.ai.ability.ExploreAi;
import forge.ai.simulation.GameStateEvaluator.Score; import forge.ai.simulation.GameStateEvaluator.Score;
import forge.game.Game; import forge.game.Game;
import forge.game.ability.ApiType;
import forge.game.ability.effects.CharmEffect; import forge.game.ability.effects.CharmEffect;
import forge.game.card.Card; import forge.game.card.*;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.cost.Cost; import forge.game.cost.Cost;
import forge.game.phase.PhaseType; import forge.game.phase.PhaseType;
import forge.game.player.Player; import forge.game.player.Player;
@@ -25,6 +19,11 @@ import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility; import forge.game.spellability.SpellAbility;
import forge.game.spellability.SpellAbilityCondition; import forge.game.spellability.SpellAbilityCondition;
import forge.game.zone.ZoneType; import forge.game.zone.ZoneType;
import forge.util.TextUtil;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class SpellAbilityPicker { public class SpellAbilityPicker {
private Game game; private Game game;
@@ -285,7 +284,7 @@ public class SpellAbilityPicker {
saString = sa.toString(); saString = sa.toString();
String cardName = sa.getHostCard().getName(); String cardName = sa.getHostCard().getName();
if (!cardName.isEmpty()) { if (!cardName.isEmpty()) {
saString = saString.replace(cardName, "<$>"); saString = TextUtil.fastReplace(saString, cardName, "<$>");
} }
if (saString.length() > 40) { if (saString.length() > 40) {
saString = saString.substring(0, 40) + "..."; saString = saString.substring(0, 40) + "...";
@@ -431,7 +430,11 @@ public class SpellAbilityPicker {
return card; return card;
} }
} }
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider); if (sa.getApi() == ApiType.Explore) {
return ExploreAi.shouldPutInGraveyard(fetchList, decider);
} else {
return ChangeZoneAi.chooseCardToHiddenOriginChangeZone(destination, origin, sa, fetchList, player2, decider);
}
} }
public CardCollectionView chooseSacrificeType(String type, SpellAbility ability, int amount) { public CardCollectionView chooseSacrificeType(String type, SpellAbility ability, int amount) {

View File

@@ -6,7 +6,7 @@
<parent> <parent>
<artifactId>forge</artifactId> <artifactId>forge</artifactId>
<groupId>forge</groupId> <groupId>forge</groupId>
<version>1.6.1</version> <version>1.6.4</version>
</parent> </parent>
<artifactId>forge-core</artifactId> <artifactId>forge-core</artifactId>

View File

@@ -3,6 +3,7 @@ package forge;
import forge.item.*; import forge.item.*;
import forge.util.FileUtil; import forge.util.FileUtil;
import forge.util.ImageUtil; import forge.util.ImageUtil;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.io.File; import java.io.File;
@@ -104,17 +105,17 @@ public final class ImageKeys {
// AE -> Ae and Ae -> AE for older cards with different file names // AE -> Ae and Ae -> AE for older cards with different file names
// on case-sensitive file systems // on case-sensitive file systems
if (filename.contains("Ae")) { if (filename.contains("Ae")) {
file = findFile(dir, filename.replace("Ae", "AE")); file = findFile(dir, TextUtil.fastReplace(filename, "Ae", "AE"));
if (file != null) { return file; } if (file != null) { return file; }
} else if (filename.contains("AE")) { } else if (filename.contains("AE")) {
file = findFile(dir, filename.replace("AE", "Ae")); file = findFile(dir, TextUtil.fastReplace(filename, "AE", "Ae"));
if (file != null) { return file; } if (file != null) { return file; }
} }
// some S00 cards are really part of 6ED // some S00 cards are really part of 6ED
String s2kAlias = getSetFolder("S00"); String s2kAlias = getSetFolder("S00");
if (filename.startsWith(s2kAlias)) { if (filename.startsWith(s2kAlias)) {
file = findFile(dir, filename.replace(s2kAlias, getSetFolder("6ED"))); file = findFile(dir, TextUtil.fastReplace(filename, s2kAlias, getSetFolder("6ED")));
if (file != null) { return file; } if (file != null) { return file; }
} }

View File

@@ -637,9 +637,9 @@ public final class CardDb implements ICardDatabase, IDeckGenPool {
} }
if (cardRarity == CardRarity.Unknown) { if (cardRarity == CardRarity.Unknown) {
System.err.println(String.format("An unknown card found when loading Forge decks: \"%s\" Forge does not know of such a card's existence. Have you mistyped the card name?", cardName)); System.err.println(TextUtil.concatWithSpace("An unknown card found when loading Forge decks:", TextUtil.enclosedDoubleQuote(cardName), "Forge does not know of such a card's existence. Have you mistyped the card name?"));
} else { } else {
System.err.println(String.format("An unsupported card was requested: \"%s\" from \"%s\" set. We're sorry, but you cannot use this card yet.", request.cardName, cardEdition.getName())); System.err.println(TextUtil.concatWithSpace("An unsupported card was requested:", TextUtil.enclosedDoubleQuote(request.cardName), "from", TextUtil.enclosedDoubleQuote(cardEdition.getName()), "set. We're sorry, but you cannot use this card yet."));
} }
return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1); return new PaperCard(CardRules.getUnsupportedCardNamed(request.cardName), cardEdition.getCode(), cardRarity, 1);

View File

@@ -27,11 +27,7 @@ import forge.card.CardDb.SetPreference;
import forge.deck.CardPool; import forge.deck.CardPool;
import forge.item.PaperCard; import forge.item.PaperCard;
import forge.item.SealedProduct; import forge.item.SealedProduct;
import forge.util.Aggregates; import forge.util.*;
import forge.util.FileSection;
import forge.util.FileUtil;
import forge.util.IItemReader;
import forge.util.MyRandom;
import forge.util.storage.StorageBase; import forge.util.storage.StorageBase;
import forge.util.storage.StorageReaderBase; import forge.util.storage.StorageReaderBase;
import forge.util.storage.StorageReaderFolder; import forge.util.storage.StorageReaderFolder;
@@ -307,7 +303,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
enumType = Type.valueOf(type.toUpperCase(Locale.ENGLISH)); enumType = Type.valueOf(type.toUpperCase(Locale.ENGLISH));
} catch (IllegalArgumentException ignored) { } catch (IllegalArgumentException ignored) {
// ignore; type will get UNKNOWN // ignore; type will get UNKNOWN
System.err.println(String.format("Ignoring unknown type in set definitions: name: %s; type: %s", res.name, type)); System.err.println(TextUtil.concatWithSpace("Ignoring unknown type in set definitions: name:", TextUtil.addSuffix(res.name, ";"), "type:", type));
} }
} }
res.type = enumType; res.type = enumType;
@@ -393,7 +389,7 @@ public final class CardEdition implements Comparable<CardEdition> { // immutable
public CardEdition getEditionByCodeOrThrow(final String code) { public CardEdition getEditionByCodeOrThrow(final String code) {
final CardEdition set = this.get(code); final CardEdition set = this.get(code);
if (null == set) { if (null == set) {
throw new RuntimeException(String.format("Edition with code '%s' not found", code)); throw new RuntimeException(TextUtil.concatWithSpace("Edition with code", TextUtil.enclosedSingleQuote(code), "not found"));
} }
return set; return set;
} }

View File

@@ -1,6 +1,7 @@
package forge.card; package forge.card;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import java.util.*; import java.util.*;
@@ -89,7 +90,7 @@ final class CardFace implements ICardFace {
void setPtText(String value) { void setPtText(String value) {
final int slashPos = value.indexOf('/'); final int slashPos = value.indexOf('/');
if (slashPos == -1) { if (slashPos == -1) {
throw new RuntimeException(String.format("Creature '%s' has bad p/t stats", this.getName())); throw new RuntimeException(TextUtil.concatWithSpace("Creature", TextUtil.enclosedSingleQuote(this.getName()),"has bad p/t stats"));
} }
boolean negPower = value.charAt(0) == '-'; boolean negPower = value.charAt(0) == '-';
boolean negToughness = value.charAt(slashPos + 1) == '-'; boolean negToughness = value.charAt(slashPos + 1) == '-';

View File

@@ -17,13 +17,14 @@
*/ */
package forge.card; package forge.card;
import java.util.StringTokenizer; import com.google.common.collect.Iterables;
import org.apache.commons.lang3.StringUtils;
import forge.card.mana.IParserManaCost; import forge.card.mana.IParserManaCost;
import forge.card.mana.ManaCost; import forge.card.mana.ManaCost;
import forge.card.mana.ManaCostShard; import forge.card.mana.ManaCostShard;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import java.util.StringTokenizer;
/** /**
* A collection of methods containing full * A collection of methods containing full
@@ -198,6 +199,10 @@ public final class CardRules implements ICardCharacteristics {
return mainPart.getOracleText().contains("can be your commander"); return mainPart.getOracleText().contains("can be your commander");
} }
public boolean canBePartnerCommander() {
return canBeCommander() && Iterables.contains(mainPart.getKeywords(), "Partner");
}
public String getMeldWith() { public String getMeldWith() {
return meldWith; return meldWith;
} }
@@ -217,10 +222,10 @@ public final class CardRules implements ICardCharacteristics {
public void setVanguardProperties(String pt) { public void setVanguardProperties(String pt) {
final int slashPos = pt == null ? -1 : pt.indexOf('/'); final int slashPos = pt == null ? -1 : pt.indexOf('/');
if (slashPos == -1) { if (slashPos == -1) {
throw new RuntimeException(String.format("Vanguard '%s' has bad hand/life stats", this.getName())); throw new RuntimeException(TextUtil.concatWithSpace("Vanguard", TextUtil.enclosedSingleQuote(this.getName()), "has bad hand/life stats"));
} }
this.deltaHand = Integer.parseInt(pt.substring(0, slashPos).replace("+", "")); this.deltaHand = Integer.parseInt(TextUtil.fastReplace(pt.substring(0, slashPos), "+", ""));
this.deltaLife = Integer.parseInt(pt.substring(slashPos+1).replace("+", "")); this.deltaLife = Integer.parseInt(TextUtil.fastReplace(pt.substring(slashPos+1), "+", ""));
} }
// Downloadable image // Downloadable image

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