Compare commits

..

302 Commits

Author SHA1 Message Date
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
522 changed files with 8944 additions and 3757 deletions

198
.gitattributes vendored
View File

@@ -13,6 +13,7 @@ forge-ai/.settings/org.eclipse.core.resources.prefs -text
forge-ai/.settings/org.eclipse.jdt.core.prefs -text
forge-ai/.settings/org.eclipse.m2e.core.prefs -text
forge-ai/pom.xml -text
forge-ai/src/main/java/forge/ai/AIOption.java -text
forge-ai/src/main/java/forge/ai/AiAttackController.java svneol=native#text/plain
forge-ai/src/main/java/forge/ai/AiBlockController.java svneol=native#text/plain
forge-ai/src/main/java/forge/ai/AiCardMemory.java -text
@@ -172,14 +173,11 @@ forge-core/.settings/org.eclipse.core.resources.prefs -text
forge-core/.settings/org.eclipse.jdt.core.prefs -text
forge-core/.settings/org.eclipse.m2e.core.prefs -text
forge-core/pom.xml -text
forge-core/src/main/java/forge/AIOption.java -text
forge-core/src/main/java/forge/CardStorageReader.java -text
forge-core/src/main/java/forge/FTrace.java -text
forge-core/src/main/java/forge/ImageKeys.java -text
forge-core/src/main/java/forge/LobbyPlayer.java -text
forge-core/src/main/java/forge/StaticData.java -text
forge-core/src/main/java/forge/card/BoosterGenerator.java svneol=native#text/plain
forge-core/src/main/java/forge/card/BoosterSlots.java -text
forge-core/src/main/java/forge/card/CardAiHints.java -text
forge-core/src/main/java/forge/card/CardChangedType.java -text
forge-core/src/main/java/forge/card/CardDb.java -text
@@ -199,10 +197,8 @@ forge-core/src/main/java/forge/card/ICardCharacteristics.java -text
forge-core/src/main/java/forge/card/ICardDatabase.java -text
forge-core/src/main/java/forge/card/ICardFace.java -text
forge-core/src/main/java/forge/card/ICardRawAbilites.java -text
forge-core/src/main/java/forge/card/IUnOpenedProduct.java -text
forge-core/src/main/java/forge/card/MagicColor.java -text
forge-core/src/main/java/forge/card/PrintSheet.java -text
forge-core/src/main/java/forge/card/UnOpenedProduct.java -text
forge-core/src/main/java/forge/card/mana/IParserManaCost.java -text
forge-core/src/main/java/forge/card/mana/ManaAtom.java -text
forge-core/src/main/java/forge/card/mana/ManaCost.java -text
@@ -243,6 +239,10 @@ forge-core/src/main/java/forge/item/PaperToken.java -text
forge-core/src/main/java/forge/item/PreconDeck.java -text
forge-core/src/main/java/forge/item/SealedProduct.java -text
forge-core/src/main/java/forge/item/TournamentPack.java -text
forge-core/src/main/java/forge/item/generation/BoosterGenerator.java -text
forge-core/src/main/java/forge/item/generation/BoosterSlots.java -text
forge-core/src/main/java/forge/item/generation/IUnOpenedProduct.java -text
forge-core/src/main/java/forge/item/generation/UnOpenedProduct.java -text
forge-core/src/main/java/forge/item/package-info.java -text
forge-core/src/main/java/forge/util/Aggregates.java -text
forge-core/src/main/java/forge/util/Base64Coder.java -text
@@ -304,6 +304,7 @@ forge-game/src/main/java/forge/GameCommand.java svneol=native#text/plain
forge-game/src/main/java/forge/game/CardTraitBase.java -text
forge-game/src/main/java/forge/game/CardTraitPredicates.java -text svneol=unset#text/plain
forge-game/src/main/java/forge/game/Direction.java -text
forge-game/src/main/java/forge/game/ForgeScript.java -text
forge-game/src/main/java/forge/game/Game.java -text
forge-game/src/main/java/forge/game/GameAction.java svneol=native#text/plain
forge-game/src/main/java/forge/game/GameActionUtil.java svneol=native#text/plain
@@ -494,6 +495,7 @@ forge-game/src/main/java/forge/game/card/CardLists.java svneol=native#text/plain
forge-game/src/main/java/forge/game/card/CardPlayOption.java -text
forge-game/src/main/java/forge/game/card/CardPowerToughness.java svneol=native#text/plain
forge-game/src/main/java/forge/game/card/CardPredicates.java svneol=native#text/plain
forge-game/src/main/java/forge/game/card/CardProperty.java -text
forge-game/src/main/java/forge/game/card/CardShields.java -text
forge-game/src/main/java/forge/game/card/CardState.java -text
forge-game/src/main/java/forge/game/card/CardUtil.java svneol=native#text/plain
@@ -633,6 +635,7 @@ forge-game/src/main/java/forge/game/player/PlayerCollection.java -text svneol=un
forge-game/src/main/java/forge/game/player/PlayerController.java -text
forge-game/src/main/java/forge/game/player/PlayerOutcome.java -text
forge-game/src/main/java/forge/game/player/PlayerPredicates.java -text svneol=unset#text/plain
forge-game/src/main/java/forge/game/player/PlayerProperty.java -text
forge-game/src/main/java/forge/game/player/PlayerStatistics.java -text
forge-game/src/main/java/forge/game/player/PlayerView.java -text
forge-game/src/main/java/forge/game/player/RegisteredPlayer.java -text
@@ -688,6 +691,7 @@ forge-game/src/main/java/forge/game/staticability/StaticAbilityLayer.java -text
forge-game/src/main/java/forge/game/staticability/StaticAbilityPreventDamage.java svneol=native#text/plain
forge-game/src/main/java/forge/game/staticability/package-info.java svneol=native#text/plain
forge-game/src/main/java/forge/game/trigger/Trigger.java svneol=native#text/plain
forge-game/src/main/java/forge/game/trigger/TriggerAbandoned.java -text
forge-game/src/main/java/forge/game/trigger/TriggerAlways.java svneol=native#text/plain
forge-game/src/main/java/forge/game/trigger/TriggerAttached.java -text
forge-game/src/main/java/forge/game/trigger/TriggerAttackerBlocked.java svneol=native#text/plain
@@ -1495,6 +1499,7 @@ forge-gui/res/blockdata/formats.txt -text
forge-gui/res/blockdata/printsheets.txt -text
forge-gui/res/blockdata/starters.txt -text
forge-gui/res/cardsfolder/a/a_display_of_my_dark_power.txt -text
forge-gui/res/cardsfolder/a/a_reckoning_approaches.txt -text
forge-gui/res/cardsfolder/a/abandon_hope.txt -text
forge-gui/res/cardsfolder/a/abandon_reason.txt -text
forge-gui/res/cardsfolder/a/abandoned_outpost.txt svneol=native#text/plain
@@ -2064,6 +2069,7 @@ forge-gui/res/cardsfolder/a/arachnoid.txt svneol=native#text/plain
forge-gui/res/cardsfolder/a/arachnus_spinner.txt -text
forge-gui/res/cardsfolder/a/arachnus_web.txt -text
forge-gui/res/cardsfolder/a/aradara_express.txt -text
forge-gui/res/cardsfolder/a/arahbo_roar_of_the_world.txt -text
forge-gui/res/cardsfolder/a/arashi_the_sky_asunder.txt svneol=native#text/plain
forge-gui/res/cardsfolder/a/arashin_cleric.txt -text
forge-gui/res/cardsfolder/a/arashin_foremost.txt -text
@@ -2479,6 +2485,7 @@ forge-gui/res/cardsfolder/b/bakis_curse.txt -text
forge-gui/res/cardsfolder/b/baku_altar.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bala_ged_scorpion.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bala_ged_thief.txt -text
forge-gui/res/cardsfolder/b/balan_wandering_knight.txt -text
forge-gui/res/cardsfolder/b/balance.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/balance_of_power.txt -text
forge-gui/res/cardsfolder/b/balancing_act.txt -text
@@ -2968,6 +2975,7 @@ forge-gui/res/cardsfolder/b/bloodfire_infusion.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodfire_kavu.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodfire_mentor.txt -text
forge-gui/res/cardsfolder/b/bloodflow_connoisseur.txt -text
forge-gui/res/cardsfolder/b/bloodforged_battle_axe.txt -text
forge-gui/res/cardsfolder/b/bloodfray_giant.txt -text
forge-gui/res/cardsfolder/b/bloodghast.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodgift_demon.txt -text
@@ -2979,6 +2987,7 @@ forge-gui/res/cardsfolder/b/bloodhusk_ritualist.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodied_ghost.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodletter_quill.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodline_keeper_lord_of_lineage.txt -text
forge-gui/res/cardsfolder/b/bloodline_necromancer.txt -text
forge-gui/res/cardsfolder/b/bloodline_shaman.txt -text
forge-gui/res/cardsfolder/b/bloodlord_of_vaasgoth.txt -text
forge-gui/res/cardsfolder/b/bloodlust_inciter.txt -text
@@ -2999,6 +3008,7 @@ forge-gui/res/cardsfolder/b/bloodspore_thrinax.txt -text
forge-gui/res/cardsfolder/b/bloodstained_mire.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodstoke_howler.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodstone_cameo.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodsworn_steward.txt -text
forge-gui/res/cardsfolder/b/bloodthirsty_ogre.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodthorn_taunter.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bloodthrone_vampire.txt svneol=native#text/plain
@@ -3100,6 +3110,7 @@ forge-gui/res/cardsfolder/b/boneshard_slasher.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bonesplitter.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bonesplitter_sliver.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bonethorn_valesk.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/boneyard_scourge.txt -text
forge-gui/res/cardsfolder/b/boneyard_wurm.txt -text
forge-gui/res/cardsfolder/b/bonfire_of_the_damned.txt -text
forge-gui/res/cardsfolder/b/bontu_the_glorified.txt -text
@@ -3173,6 +3184,7 @@ forge-gui/res/cardsfolder/b/bounty_hunter.txt svneol=native#text/plain
forge-gui/res/cardsfolder/b/bounty_of_the_hunt.txt -text
forge-gui/res/cardsfolder/b/bounty_of_the_luxa.txt -text
forge-gui/res/cardsfolder/b/bow_of_nylea.txt -text
forge-gui/res/cardsfolder/b/bow_to_my_command.txt -text
forge-gui/res/cardsfolder/b/bower_passage.txt -text
forge-gui/res/cardsfolder/b/brace_for_impact.txt -text
forge-gui/res/cardsfolder/b/brackwater_elemental.txt svneol=native#text/plain
@@ -3688,7 +3700,9 @@ forge-gui/res/cardsfolder/c/ceta_disciple.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/ceta_sanctuary.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/cetavolver.txt -text
forge-gui/res/cardsfolder/c/chain_lightning.txt -text
forge-gui/res/cardsfolder/c/chain_of_acid.txt -text
forge-gui/res/cardsfolder/c/chain_of_plasma.txt -text
forge-gui/res/cardsfolder/c/chain_of_silence.txt -text
forge-gui/res/cardsfolder/c/chain_of_smog.txt -text
forge-gui/res/cardsfolder/c/chain_of_vapor.txt -text
forge-gui/res/cardsfolder/c/chain_reaction.txt svneol=native#text/plain
@@ -3837,6 +3851,7 @@ forge-gui/res/cardsfolder/c/choking_sands.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/choking_tethers.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/choking_vines.txt -text
forge-gui/res/cardsfolder/c/choose_your_champion.txt -text
forge-gui/res/cardsfolder/c/choose_your_demise.txt -text
forge-gui/res/cardsfolder/c/chord_of_calling.txt -text
forge-gui/res/cardsfolder/c/chorus_of_might.txt -text
forge-gui/res/cardsfolder/c/chorus_of_the_conclave.txt -text
@@ -4364,6 +4379,7 @@ forge-gui/res/cardsfolder/c/crib_swap.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/crime_punishment.txt -text
forge-gui/res/cardsfolder/c/crimson_acolyte.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/crimson_hellkite.txt -text
forge-gui/res/cardsfolder/c/crimson_honor_guard.txt -text
forge-gui/res/cardsfolder/c/crimson_kobolds.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/crimson_mage.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/crimson_manticore.txt svneol=native#text/plain
@@ -4517,15 +4533,18 @@ forge-gui/res/cardsfolder/c/curiosity.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/curious_homunculus_voracious_reader.txt -text
forge-gui/res/cardsfolder/c/curse_artifact.txt -text
forge-gui/res/cardsfolder/c/curse_of_bloodletting.txt -text
forge-gui/res/cardsfolder/c/curse_of_bounty.txt -text
forge-gui/res/cardsfolder/c/curse_of_chains.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/curse_of_chaos.txt -text
forge-gui/res/cardsfolder/c/curse_of_deaths_hold.txt -text
forge-gui/res/cardsfolder/c/curse_of_disturbance.txt -text
forge-gui/res/cardsfolder/c/curse_of_echoes.txt -text
forge-gui/res/cardsfolder/c/curse_of_exhaustion.txt -text
forge-gui/res/cardsfolder/c/curse_of_inertia.txt -text
forge-gui/res/cardsfolder/c/curse_of_marit_lage.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/curse_of_misfortunes.txt -text
forge-gui/res/cardsfolder/c/curse_of_oblivion.txt -text
forge-gui/res/cardsfolder/c/curse_of_opulence.txt -text
forge-gui/res/cardsfolder/c/curse_of_predation.txt -text
forge-gui/res/cardsfolder/c/curse_of_shallow_graves.txt -text
forge-gui/res/cardsfolder/c/curse_of_stalked_prey.txt -text
@@ -4536,6 +4555,8 @@ forge-gui/res/cardsfolder/c/curse_of_the_nightly_hunt.txt -text
forge-gui/res/cardsfolder/c/curse_of_the_pierced_heart.txt -text
forge-gui/res/cardsfolder/c/curse_of_the_swine.txt -text
forge-gui/res/cardsfolder/c/curse_of_thirst.txt -text
forge-gui/res/cardsfolder/c/curse_of_verbosity.txt -text
forge-gui/res/cardsfolder/c/curse_of_vitality.txt -text
forge-gui/res/cardsfolder/c/curse_of_wizardry.txt svneol=native#text/plain
forge-gui/res/cardsfolder/c/cursebreak.txt -text
forge-gui/res/cardsfolder/c/cursecatcher.txt svneol=native#text/plain
@@ -4928,6 +4949,7 @@ forge-gui/res/cardsfolder/d/delay.txt -text
forge-gui/res/cardsfolder/d/delaying_shield.txt -text
forge-gui/res/cardsfolder/d/delifs_cone.txt -text
forge-gui/res/cardsfolder/d/delifs_cube.txt -text
forge-gui/res/cardsfolder/d/delight_in_the_hunt.txt -text
forge-gui/res/cardsfolder/d/delirium.txt svneol=native#text/plain
forge-gui/res/cardsfolder/d/delirium_skeins.txt svneol=native#text/plain
forge-gui/res/cardsfolder/d/delraich.txt svneol=native#text/plain
@@ -5186,6 +5208,7 @@ forge-gui/res/cardsfolder/d/display_of_dominance.txt -text
forge-gui/res/cardsfolder/d/disposal_mummy.txt -text
forge-gui/res/cardsfolder/d/dispossess.txt -text
forge-gui/res/cardsfolder/d/disrupt.txt svneol=native#text/plain
forge-gui/res/cardsfolder/d/disrupt_decorum.txt -text
forge-gui/res/cardsfolder/d/disrupting_scepter.txt svneol=native#text/plain
forge-gui/res/cardsfolder/d/disrupting_shoal.txt -text
forge-gui/res/cardsfolder/d/disruption_aura.txt -text
@@ -5642,6 +5665,7 @@ forge-gui/res/cardsfolder/e/echoing_decay.txt svneol=native#text/plain
forge-gui/res/cardsfolder/e/echoing_ruin.txt svneol=native#text/plain
forge-gui/res/cardsfolder/e/echoing_truth.txt svneol=native#text/plain
forge-gui/res/cardsfolder/e/eddytrail_hawk.txt -text
forge-gui/res/cardsfolder/e/edgar_markov.txt -text
forge-gui/res/cardsfolder/e/edge_of_autumn.txt svneol=native#text/plain
forge-gui/res/cardsfolder/e/edge_of_malacol.txt -text
forge-gui/res/cardsfolder/e/edge_of_the_divinity.txt svneol=native#text/plain
@@ -5970,6 +5994,7 @@ forge-gui/res/cardsfolder/e/erosion.txt -text
forge-gui/res/cardsfolder/e/errand_of_duty.txt -text
forge-gui/res/cardsfolder/e/errant_doomsayers.txt svneol=native#text/plain
forge-gui/res/cardsfolder/e/errant_ephemeron.txt svneol=native#text/plain
forge-gui/res/cardsfolder/e/errant_minion.txt -text
forge-gui/res/cardsfolder/e/errantry.txt -text
forge-gui/res/cardsfolder/e/erratic_explosion.txt svneol=native#text/plain
forge-gui/res/cardsfolder/e/erratic_mutation.txt -text
@@ -6058,6 +6083,7 @@ forge-gui/res/cardsfolder/e/everlasting_torment.txt -text
forge-gui/res/cardsfolder/e/evermind.txt -text
forge-gui/res/cardsfolder/e/evernight_shade.txt -text
forge-gui/res/cardsfolder/e/evershrike.txt -text
forge-gui/res/cardsfolder/e/every_dream_a_nightmare.txt -text
forge-gui/res/cardsfolder/e/every_hope_shall_vanish.txt -text
forge-gui/res/cardsfolder/e/every_last_vestige_shall_rot.txt -text
forge-gui/res/cardsfolder/e/evil_comes_to_fruition.txt -text
@@ -6690,6 +6716,7 @@ forge-gui/res/cardsfolder/f/foot_soldiers.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/footbottom_feast.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/foothill_guide.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/footsteps_of_the_goryo.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/for_each_of_you_a_gift.txt -text
forge-gui/res/cardsfolder/f/foratog.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/forbid.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/forbidden_alchemy.txt -text
@@ -6756,6 +6783,7 @@ forge-gui/res/cardsfolder/f/fortitude.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/fortress_crab.txt -text
forge-gui/res/cardsfolder/f/fortress_cyclops.txt -text
forge-gui/res/cardsfolder/f/fortuitous_find.txt -text
forge-gui/res/cardsfolder/f/fortunate_few.txt -text
forge-gui/res/cardsfolder/f/fortune_thief.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/fortunes_favor.txt -text
forge-gui/res/cardsfolder/f/fossil_find.txt -text
@@ -6782,6 +6810,7 @@ forge-gui/res/cardsfolder/f/fountain_watch.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/fourth_bridge_prowler.txt -text
forge-gui/res/cardsfolder/f/foxfire.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/foxfire_oak.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/fractured_identity.txt -text
forge-gui/res/cardsfolder/f/fractured_loyalty.txt svneol=native#text/plain
forge-gui/res/cardsfolder/f/fractured_powerstone.txt -text
forge-gui/res/cardsfolder/f/fracturing_gust.txt svneol=native#text/plain
@@ -6921,6 +6950,7 @@ forge-gui/res/cardsfolder/g/gaeas_touch.txt -text
forge-gui/res/cardsfolder/g/gahiji_honored_one.txt -text
forge-gui/res/cardsfolder/g/gainsay.txt svneol=native#text/plain
forge-gui/res/cardsfolder/g/gale_force.txt svneol=native#text/plain
forge-gui/res/cardsfolder/g/galecaster_colossus.txt -text
forge-gui/res/cardsfolder/g/galepowder_mage.txt svneol=native#text/plain
forge-gui/res/cardsfolder/g/galerider_sliver.txt -text
forge-gui/res/cardsfolder/g/galestrike.txt -text
@@ -7855,6 +7885,7 @@ forge-gui/res/cardsfolder/h/hamlet_captain.txt -text
forge-gui/res/cardsfolder/h/hamletback_goliath.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hammer_mage.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hammer_of_bogardan.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hammer_of_nazahn.txt -text
forge-gui/res/cardsfolder/h/hammer_of_purphoros.txt -text
forge-gui/res/cardsfolder/h/hammer_of_ruin.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hammerfist_giant.txt svneol=native#text/plain
@@ -7963,6 +7994,7 @@ forge-gui/res/cardsfolder/h/havoc_festival.txt -text
forge-gui/res/cardsfolder/h/havoc_sower.txt -text
forge-gui/res/cardsfolder/h/hawkeater_moth.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hazardous_conditions.txt -text
forge-gui/res/cardsfolder/h/hazduhr_the_abbot.txt -text
forge-gui/res/cardsfolder/h/haze_frog.txt -text
forge-gui/res/cardsfolder/h/haze_of_pollen.txt -text
forge-gui/res/cardsfolder/h/haze_of_rage.txt svneol=native#text/plain
@@ -8048,6 +8080,7 @@ forge-gui/res/cardsfolder/h/heidar_rimewind_master.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/heightened_awareness.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/heir_of_falkenrath_heir_to_the_night.txt -text
forge-gui/res/cardsfolder/h/heir_of_the_wilds.txt -text
forge-gui/res/cardsfolder/h/heirloom_blade.txt -text
forge-gui/res/cardsfolder/h/heirs_of_stromkirk.txt -text
forge-gui/res/cardsfolder/h/hekma_sentinels.txt -text
forge-gui/res/cardsfolder/h/heliod_god_of_the_sun.txt -text
@@ -8102,6 +8135,7 @@ forge-gui/res/cardsfolder/h/herald_of_the_host.txt -text
forge-gui/res/cardsfolder/h/herald_of_the_pantheon.txt -text
forge-gui/res/cardsfolder/h/herald_of_torment.txt -text
forge-gui/res/cardsfolder/h/herald_of_war.txt -text
forge-gui/res/cardsfolder/h/heralds_horn.txt -text
forge-gui/res/cardsfolder/h/herbal_poultice.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/herd_gnarr.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/herdchaser_dragon.txt -text
@@ -8369,6 +8403,7 @@ forge-gui/res/cardsfolder/h/hunger_of_the_howlpack.txt -text
forge-gui/res/cardsfolder/h/hunger_of_the_nim.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hungering_yeti.txt -text
forge-gui/res/cardsfolder/h/hungry_flames.txt -text
forge-gui/res/cardsfolder/h/hungry_lynx.txt -text
forge-gui/res/cardsfolder/h/hungry_mist.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hungry_spriggan.txt svneol=native#text/plain
forge-gui/res/cardsfolder/h/hunt_down.txt -text
@@ -8563,6 +8598,7 @@ forge-gui/res/cardsfolder/i/in_oketras_name.txt -text
forge-gui/res/cardsfolder/i/in_the_eye_of_chaos.txt svneol=native#text/plain
forge-gui/res/cardsfolder/i/in_the_web_of_war.txt svneol=native#text/plain
forge-gui/res/cardsfolder/i/inaction_injunction.txt -text
forge-gui/res/cardsfolder/i/inalla_archmage_ritualist.txt -text
forge-gui/res/cardsfolder/i/iname_as_one.txt svneol=native#text/plain
forge-gui/res/cardsfolder/i/iname_death_aspect.txt -text
forge-gui/res/cardsfolder/i/iname_life_aspect.txt -text svneol=unset#text/plain
@@ -8833,6 +8869,7 @@ forge-gui/res/cardsfolder/i/ixidors_will.txt svneol=native#text/plain
forge-gui/res/cardsfolder/i/ixidron.txt -text
forge-gui/res/cardsfolder/i/izzet_boilerworks.txt svneol=native#text/plain
forge-gui/res/cardsfolder/i/izzet_charm.txt -text
forge-gui/res/cardsfolder/i/izzet_chemister.txt -text
forge-gui/res/cardsfolder/i/izzet_chronarch.txt svneol=native#text/plain
forge-gui/res/cardsfolder/i/izzet_cluestone.txt -text
forge-gui/res/cardsfolder/i/izzet_guildgate.txt -text
@@ -9175,6 +9212,7 @@ forge-gui/res/cardsfolder/k/kembas_skyguard.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/kemuri_onna.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/kentaro_the_smiling_cat.txt -text
forge-gui/res/cardsfolder/k/keranos_god_of_storms.txt -text
forge-gui/res/cardsfolder/k/kess_dissident_mage.txt -text
forge-gui/res/cardsfolder/k/kessig.txt -text
forge-gui/res/cardsfolder/k/kessig_cagebreakers.txt -text
forge-gui/res/cardsfolder/k/kessig_dire_swine.txt -text
@@ -9200,6 +9238,7 @@ forge-gui/res/cardsfolder/k/kher_keep.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/kheru_bloodsucker.txt -text
forge-gui/res/cardsfolder/k/kheru_dreadmaw.txt -text
forge-gui/res/cardsfolder/k/kheru_lich_lord.txt -text
forge-gui/res/cardsfolder/k/kheru_mind_eater.txt -text
forge-gui/res/cardsfolder/k/kheru_spellsnatcher.txt -text
forge-gui/res/cardsfolder/k/kiki_jiki_mirror_breaker.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/kiku_nights_flower.txt svneol=native#text/plain
@@ -9223,6 +9262,11 @@ forge-gui/res/cardsfolder/k/kindle.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/kindle_the_carnage.txt -text
forge-gui/res/cardsfolder/k/kindled_fury.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/kindly_stranger_demon_possessed_witch.txt -text
forge-gui/res/cardsfolder/k/kindred_boon.txt -text
forge-gui/res/cardsfolder/k/kindred_charge.txt -text
forge-gui/res/cardsfolder/k/kindred_discovery.txt -text
forge-gui/res/cardsfolder/k/kindred_dominance.txt -text
forge-gui/res/cardsfolder/k/kindred_summons.txt -text
forge-gui/res/cardsfolder/k/king_cheetah.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/king_crab.txt svneol=native#text/plain
forge-gui/res/cardsfolder/k/king_macar_the_gold_cursed.txt -text
@@ -9675,7 +9719,9 @@ forge-gui/res/cardsfolder/l/lich_lord_of_unx.txt svneol=native#text/plain
forge-gui/res/cardsfolder/l/lichenthrope.txt svneol=native#text/plain
forge-gui/res/cardsfolder/l/lichs_mirror.txt -text
forge-gui/res/cardsfolder/l/lichs_tomb.txt -text
forge-gui/res/cardsfolder/l/licia_sanguine_tribune.txt -text
forge-gui/res/cardsfolder/l/liege_of_the_axe.txt svneol=native#text/plain
forge-gui/res/cardsfolder/l/liege_of_the_hollows.txt -text
forge-gui/res/cardsfolder/l/liege_of_the_pit.txt svneol=native#text/plain
forge-gui/res/cardsfolder/l/liege_of_the_tangle.txt svneol=native#text/plain
forge-gui/res/cardsfolder/l/lieutenant_kirtar.txt svneol=native#text/plain
@@ -10058,6 +10104,7 @@ forge-gui/res/cardsfolder/m/magus_of_the_disk.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/magus_of_the_future.txt -text
forge-gui/res/cardsfolder/m/magus_of_the_jar.txt -text
forge-gui/res/cardsfolder/m/magus_of_the_library.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/magus_of_the_mind.txt -text
forge-gui/res/cardsfolder/m/magus_of_the_mirror.txt -text
forge-gui/res/cardsfolder/m/magus_of_the_moat.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/magus_of_the_moon.txt svneol=native#text/plain
@@ -10068,12 +10115,14 @@ forge-gui/res/cardsfolder/m/magus_of_the_vineyard.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/magus_of_the_wheel.txt -text
forge-gui/res/cardsfolder/m/magus_of_the_will.txt -text
forge-gui/res/cardsfolder/m/mahamoti_djinn.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mairsil_the_pretender.txt -text
forge-gui/res/cardsfolder/m/majestic_myriarch.txt -text
forge-gui/res/cardsfolder/m/major_teroh.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/make_a_stand.txt -text
forge-gui/res/cardsfolder/m/make_a_wish.txt -text
forge-gui/res/cardsfolder/m/make_mischief.txt -text
forge-gui/res/cardsfolder/m/make_obsolete.txt -text
forge-gui/res/cardsfolder/m/make_yourself_useful.txt -text
forge-gui/res/cardsfolder/m/makeshift_mannequin.txt -text svneol=unset#text/plain
forge-gui/res/cardsfolder/m/makeshift_mauler.txt -text
forge-gui/res/cardsfolder/m/makindi_aeronaut.txt -text
@@ -10305,6 +10354,7 @@ forge-gui/res/cardsfolder/m/mastery_of_the_unseen.txt -text
forge-gui/res/cardsfolder/m/masticore.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/masumaro_first_to_live.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/matca_rioters.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mathas_fiend_seeker.txt -text
forge-gui/res/cardsfolder/m/matopi_golem.txt -text
forge-gui/res/cardsfolder/m/matsu_tribe_birdstalker.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/matsu_tribe_decoy.txt -text
@@ -10623,6 +10673,7 @@ forge-gui/res/cardsfolder/m/mirri.txt -text
forge-gui/res/cardsfolder/m/mirri_cat_warrior.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mirri_the_cursed.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mirri_the_cursed_avatar.txt -text
forge-gui/res/cardsfolder/m/mirri_weatherlight_duelist.txt -text
forge-gui/res/cardsfolder/m/mirris_guile.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mirrodins_core.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mirror_entity.txt svneol=native#text/plain
@@ -10633,6 +10684,7 @@ forge-gui/res/cardsfolder/m/mirror_mad_phantasm.txt -text
forge-gui/res/cardsfolder/m/mirror_match.txt -text
forge-gui/res/cardsfolder/m/mirror_mockery.txt -text
forge-gui/res/cardsfolder/m/mirror_of_fate.txt -text
forge-gui/res/cardsfolder/m/mirror_of_the_forebears.txt -text
forge-gui/res/cardsfolder/m/mirror_sheen.txt -text
forge-gui/res/cardsfolder/m/mirror_sigil_sergeant.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mirror_strike.txt -text
@@ -10964,7 +11016,9 @@ forge-gui/res/cardsfolder/m/mwonvuli_acid_moss.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/mwonvuli_beast_tracker.txt -text
forge-gui/res/cardsfolder/m/mwonvuli_ooze.txt svneol=native#text/plain
forge-gui/res/cardsfolder/m/my_crushing_masterstroke.txt -text svneol=unset#text/plain
forge-gui/res/cardsfolder/m/my_forces_are_innumerable.txt -text
forge-gui/res/cardsfolder/m/my_genius_knows_no_bounds.txt -text
forge-gui/res/cardsfolder/m/my_laughter_echoes.txt -text
forge-gui/res/cardsfolder/m/my_undead_horde_awakens.txt -text
forge-gui/res/cardsfolder/m/my_wish_is_your_command.txt -text
forge-gui/res/cardsfolder/m/mycoid_shepherd.txt svneol=native#text/plain
@@ -11114,6 +11168,7 @@ forge-gui/res/cardsfolder/n/naya_hushblade.txt svneol=native#text/plain
forge-gui/res/cardsfolder/n/naya_panorama.txt svneol=native#text/plain
forge-gui/res/cardsfolder/n/naya_sojourners.txt svneol=native#text/plain
forge-gui/res/cardsfolder/n/naya_soulbeast.txt -text
forge-gui/res/cardsfolder/n/nazahn_revered_bladesmith.txt -text
forge-gui/res/cardsfolder/n/near_death_experience.txt svneol=native#text/plain
forge-gui/res/cardsfolder/n/nearheath_chaplain.txt -text
forge-gui/res/cardsfolder/n/nearheath_pilgrim.txt -text
@@ -11229,6 +11284,7 @@ forge-gui/res/cardsfolder/n/nevermaker.txt svneol=native#text/plain
forge-gui/res/cardsfolder/n/nevermore.txt -text
forge-gui/res/cardsfolder/n/nevinyrrals_disk.txt svneol=native#text/plain
forge-gui/res/cardsfolder/n/new_benalia.txt svneol=native#text/plain
forge-gui/res/cardsfolder/n/new_blood.txt -text
forge-gui/res/cardsfolder/n/new_frontiers.txt -text
forge-gui/res/cardsfolder/n/new_perspectives.txt -text
forge-gui/res/cardsfolder/n/new_prahv_guildmage.txt -text
@@ -11441,6 +11497,7 @@ forge-gui/res/cardsfolder/n/nyxborn_rollicker.txt -text
forge-gui/res/cardsfolder/n/nyxborn_shieldmate.txt -text
forge-gui/res/cardsfolder/n/nyxborn_triton.txt -text
forge-gui/res/cardsfolder/n/nyxborn_wolf.txt -text
forge-gui/res/cardsfolder/o/o-kagachi_vengeful_kami.txt -text
forge-gui/res/cardsfolder/o/o_naginata.txt -text
forge-gui/res/cardsfolder/o/oak_street_innkeeper.txt -text
forge-gui/res/cardsfolder/o/oaken_brawler.txt svneol=native#text/plain
@@ -11888,6 +11945,7 @@ forge-gui/res/cardsfolder/p/past_in_flames.txt -text
forge-gui/res/cardsfolder/p/patagia_golem.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/patagia_viper.txt -text
forge-gui/res/cardsfolder/p/patchwork_gnomes.txt -text
forge-gui/res/cardsfolder/p/path_of_ancestry.txt -text
forge-gui/res/cardsfolder/p/path_of_angers_flame.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/path_of_bravery.txt -text
forge-gui/res/cardsfolder/p/path_of_peace.txt svneol=native#text/plain
@@ -11908,6 +11966,7 @@ forge-gui/res/cardsfolder/p/patron_of_the_moon.txt -text
forge-gui/res/cardsfolder/p/patron_of_the_nezumi.txt -text
forge-gui/res/cardsfolder/p/patron_of_the_orochi.txt -text
forge-gui/res/cardsfolder/p/patron_of_the_valiant.txt -text
forge-gui/res/cardsfolder/p/patron_of_the_vein.txt -text
forge-gui/res/cardsfolder/p/patron_of_the_wild.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/patron_wizard.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/pattern_of_rebirth.txt svneol=native#text/plain
@@ -12285,6 +12344,7 @@ forge-gui/res/cardsfolder/p/powder_keg.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/power_armor.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/power_artifact.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/power_conduit.txt -text
forge-gui/res/cardsfolder/p/power_leak.txt -text
forge-gui/res/cardsfolder/p/power_matrix.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/power_of_fire.txt svneol=native#text/plain
forge-gui/res/cardsfolder/p/power_play.txt -text
@@ -12597,6 +12657,7 @@ forge-gui/res/cardsfolder/q/qarsi_high_priest.txt -text
forge-gui/res/cardsfolder/q/qarsi_sadist.txt -text
forge-gui/res/cardsfolder/q/qasali_ambusher.txt -text
forge-gui/res/cardsfolder/q/qasali_pridemage.txt svneol=native#text/plain
forge-gui/res/cardsfolder/q/qasali_slingers.txt -text
forge-gui/res/cardsfolder/q/quag_sickness.txt svneol=native#text/plain
forge-gui/res/cardsfolder/q/quag_vampires.txt svneol=native#text/plain
forge-gui/res/cardsfolder/q/quagmire.txt svneol=native#text/plain
@@ -12767,6 +12828,7 @@ forge-gui/res/cardsfolder/r/rally_the_peasants.txt -text
forge-gui/res/cardsfolder/r/rally_the_righteous.txt -text
forge-gui/res/cardsfolder/r/rally_the_troops.txt -text
forge-gui/res/cardsfolder/r/ramirez_depietro.txt svneol=native#text/plain
forge-gui/res/cardsfolder/r/ramos_dragon_engine.txt -text
forge-gui/res/cardsfolder/r/ramosian_captain.txt svneol=native#text/plain
forge-gui/res/cardsfolder/r/ramosian_commander.txt svneol=native#text/plain
forge-gui/res/cardsfolder/r/ramosian_lieutenant.txt svneol=native#text/plain
@@ -13799,6 +13861,7 @@ forge-gui/res/cardsfolder/s/scaled_behemoth.txt -text
forge-gui/res/cardsfolder/s/scaled_hulk.txt svneol=native#text/plain
forge-gui/res/cardsfolder/s/scaled_wurm.txt svneol=native#text/plain
forge-gui/res/cardsfolder/s/scaleguard_sentinels.txt -text
forge-gui/res/cardsfolder/s/scalelord_reckoner.txt -text
forge-gui/res/cardsfolder/s/scalpelexis.txt svneol=native#text/plain
forge-gui/res/cardsfolder/s/scandalmonger.txt svneol=native#text/plain
forge-gui/res/cardsfolder/s/scapegoat.txt svneol=native#text/plain
@@ -14329,6 +14392,7 @@ forge-gui/res/cardsfolder/s/shieldmates_blessing.txt svneol=native#text/plain
forge-gui/res/cardsfolder/s/shields_of_velis_vel.txt -text
forge-gui/res/cardsfolder/s/shifting_borders.txt -text
forge-gui/res/cardsfolder/s/shifting_loyalties.txt -text
forge-gui/res/cardsfolder/s/shifting_shadow.txt -text
forge-gui/res/cardsfolder/s/shifting_sky.txt -text
forge-gui/res/cardsfolder/s/shifting_sliver.txt svneol=native#text/plain
forge-gui/res/cardsfolder/s/shifting_wall.txt svneol=native#text/plain
@@ -15938,6 +16002,8 @@ forge-gui/res/cardsfolder/t/tahngarth_talruum_hero.txt svneol=native#text/plain
forge-gui/res/cardsfolder/t/tahngarths_glare.txt -text
forge-gui/res/cardsfolder/t/tahngarths_rage.txt svneol=native#text/plain
forge-gui/res/cardsfolder/t/taiga.txt svneol=native#text/plain
forge-gui/res/cardsfolder/t/taigam_ojutai_master.txt -text
forge-gui/res/cardsfolder/t/taigam_sidisis_hand.txt -text
forge-gui/res/cardsfolder/t/taigams_scheming.txt -text
forge-gui/res/cardsfolder/t/taigams_strike.txt -text
forge-gui/res/cardsfolder/t/tail_slash.txt -text
@@ -16079,6 +16145,7 @@ forge-gui/res/cardsfolder/t/teferis_honor_guard.txt -text
forge-gui/res/cardsfolder/t/teferis_imp.txt -text
forge-gui/res/cardsfolder/t/teferis_isle.txt -text
forge-gui/res/cardsfolder/t/teferis_moat.txt -text
forge-gui/res/cardsfolder/t/teferis_protection.txt -text
forge-gui/res/cardsfolder/t/teferis_puzzle_box.txt svneol=native#text/plain
forge-gui/res/cardsfolder/t/teferis_realm.txt -text
forge-gui/res/cardsfolder/t/teferis_response.txt svneol=native#text/plain
@@ -16271,6 +16338,7 @@ forge-gui/res/cardsfolder/t/the_scarab_god.txt -text
forge-gui/res/cardsfolder/t/the_scorpion_god.txt -text
forge-gui/res/cardsfolder/t/the_tabernacle_at_pendrell_vale.txt svneol=native#text/plain
forge-gui/res/cardsfolder/t/the_unspeakable.txt svneol=native#text/plain
forge-gui/res/cardsfolder/t/the_ur_dragon.txt -text
forge-gui/res/cardsfolder/t/the_very_soil_shall_shake.txt -text
forge-gui/res/cardsfolder/t/the_wretched.txt -text svneol=unset#text/plain
forge-gui/res/cardsfolder/t/the_zephyr_maze.txt -text
@@ -16730,6 +16798,7 @@ forge-gui/res/cardsfolder/t/travelers_amulet.txt -text
forge-gui/res/cardsfolder/t/travelers_cloak.txt -text
forge-gui/res/cardsfolder/t/traveling_philosopher.txt -text
forge-gui/res/cardsfolder/t/traveling_plague.txt -text
forge-gui/res/cardsfolder/t/traverse_the_outlands.txt -text
forge-gui/res/cardsfolder/t/traverse_the_ulvenwald.txt -text
forge-gui/res/cardsfolder/t/treacherous_link.txt svneol=native#text/plain
forge-gui/res/cardsfolder/t/treacherous_pit_dweller.txt -text
@@ -17134,10 +17203,6 @@ forge-gui/res/cardsfolder/u/utter_end.txt -text
forge-gui/res/cardsfolder/u/utvara_hellkite.txt -text
forge-gui/res/cardsfolder/u/utvara_scalper.txt svneol=native#text/plain
forge-gui/res/cardsfolder/u/uyo_silent_prophet.txt svneol=native#text/plain
forge-gui/res/cardsfolder/upcoming/ramos_dragon_engine.txt -text
forge-gui/res/cardsfolder/upcoming/taigam_ojutai_master.txt -text
forge-gui/res/cardsfolder/upcoming/the_ur_dragon.txt -text
forge-gui/res/cardsfolder/upcoming/wasitora_nekoru_queen.txt -text
forge-gui/res/cardsfolder/v/vacuumelt.txt svneol=native#text/plain
forge-gui/res/cardsfolder/v/vaevictis_asmadi.txt svneol=native#text/plain
forge-gui/res/cardsfolder/v/vagrant_plowbeasts.txt svneol=native#text/plain
@@ -17398,6 +17463,7 @@ forge-gui/res/cardsfolder/v/villagers_of_estwald_howlpack_of_estwald.txt -text
forge-gui/res/cardsfolder/v/villainous_ogre.txt svneol=native#text/plain
forge-gui/res/cardsfolder/v/villainous_wealth.txt -text
forge-gui/res/cardsfolder/v/vindicate.txt svneol=native#text/plain
forge-gui/res/cardsfolder/v/vindictive_lich.txt -text
forge-gui/res/cardsfolder/v/vindictive_mob.txt svneol=native#text/plain
forge-gui/res/cardsfolder/v/vine_dryad.txt svneol=native#text/plain
forge-gui/res/cardsfolder/v/vine_kami.txt -text
@@ -17765,6 +17831,7 @@ forge-gui/res/cardsfolder/w/wars_toll.txt -text
forge-gui/res/cardsfolder/w/warstorm_surge.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/warthog.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/wash_out.txt -text
forge-gui/res/cardsfolder/w/wasitora_nekoru_queen.txt -text
forge-gui/res/cardsfolder/w/wasp_lancer.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/wasp_of_the_bitter_end.txt -text
forge-gui/res/cardsfolder/w/waste_away.txt svneol=native#text/plain
@@ -18188,6 +18255,7 @@ forge-gui/res/cardsfolder/w/wormfang_drake.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/wormfang_manta.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/wormfang_newt.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/wormfang_turtle.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/worms_of_the_earth.txt -text
forge-gui/res/cardsfolder/w/wormwood_dryad.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/wormwood_treefolk.txt svneol=native#text/plain
forge-gui/res/cardsfolder/w/worn_powerstone.txt svneol=native#text/plain
@@ -18509,6 +18577,7 @@ forge-gui/res/conquest/planes/Amonkhet/Shefet[!!-~]Dunes/Rise[!!-~]of[!!-~]the[!
forge-gui/res/conquest/planes/Amonkhet/Shefet[!!-~]Dunes/Temmet,[!!-~]Vizier[!!-~]of[!!-~]Naktamun.dck -text
forge-gui/res/conquest/planes/Amonkhet/Shefet[!!-~]Dunes/Trial[!!-~]of[!!-~]Solidarity.dck -text
forge-gui/res/conquest/planes/Amonkhet/Shefet[!!-~]Dunes/_events.txt -text
forge-gui/res/conquest/planes/Amonkhet/cards.txt -text
forge-gui/res/conquest/planes/Amonkhet/plane_cards.txt -text
forge-gui/res/conquest/planes/Amonkhet/regions.txt -text
forge-gui/res/conquest/planes/Amonkhet/sets.txt -text
@@ -19386,11 +19455,13 @@ forge-gui/res/licenses/xstream-license.txt svneol=native#text/plain
forge-gui/res/lists/NonStackingKWList.txt svneol=native#text/plain
forge-gui/res/lists/TypeLists.txt svneol=native#text/plain
forge-gui/res/lists/achievement-images.txt -text
forge-gui/res/lists/altwin-archivements.txt -text svneol=unset#text/plain
forge-gui/res/lists/booster-images.txt svneol=native#text/plain
forge-gui/res/lists/boosterbox-images.txt -text
forge-gui/res/lists/fatpack-images.txt svneol=native#text/plain
forge-gui/res/lists/net-decks-commander.txt -text
forge-gui/res/lists/net-decks.txt -text
forge-gui/res/lists/planeswalker-archivements.txt -text svneol=unset#text/plain
forge-gui/res/lists/precon-images.txt svneol=native#text/plain
forge-gui/res/lists/quest-opponent-icons.txt svneol=native#text/plain
forge-gui/res/lists/quest-pet-token-images.txt svneol=native#text/plain
@@ -19405,6 +19476,31 @@ forge-gui/res/music/menus/Evil[!!-~]March.mp3 -text
forge-gui/res/music/menus/Heroic[!!-~]Age.mp3 -text
forge-gui/res/music/menus/Lord[!!-~]of[!!-~]the[!!-~]Land.mp3 -text
forge-gui/res/music/menus/The[!!-~]Pyre.mp3 -text
forge-gui/res/puzzle/PC_033115.pzl -text
forge-gui/res/puzzle/PC_040715.pzl -text
forge-gui/res/puzzle/PC_041415.pzl -text
forge-gui/res/puzzle/PC_042815.pzl -text
forge-gui/res/puzzle/PC_050515.pzl -text
forge-gui/res/puzzle/PC_051215.pzl -text
forge-gui/res/puzzle/PC_051915.pzl -text
forge-gui/res/puzzle/PC_052615.pzl -text
forge-gui/res/puzzle/PC_060215.pzl -text
forge-gui/res/puzzle/PC_060915.pzl -text
forge-gui/res/puzzle/PC_062315.pzl -text
forge-gui/res/puzzle/PC_063015.pzl -text
forge-gui/res/puzzle/PC_070715.pzl -text
forge-gui/res/puzzle/PC_071415.pzl -text
forge-gui/res/puzzle/PC_072115.pzl -text
forge-gui/res/puzzle/PC_072815.pzl -text
forge-gui/res/puzzle/PC_080415.pzl -text
forge-gui/res/puzzle/PC_081115.pzl -text
forge-gui/res/puzzle/PC_081815.pzl -text
forge-gui/res/puzzle/PC_082515.pzl -text
forge-gui/res/puzzle/PC_090115.pzl -text
forge-gui/res/puzzle/PC_090815.pzl -text
forge-gui/res/puzzle/PC_091515.pzl -text
forge-gui/res/puzzle/PC_092215.pzl -text
forge-gui/res/puzzle/PC_092915.pzl -text
forge-gui/res/puzzle/PC_13.pzl -text
forge-gui/res/puzzle/PC_18.pzl -text
forge-gui/res/puzzle/PC_19.pzl -text
@@ -19415,18 +19511,28 @@ forge-gui/res/puzzle/PP00.pzl -text
forge-gui/res/puzzle/PP01.pzl -text
forge-gui/res/puzzle/PP02.pzl -text
forge-gui/res/puzzle/PP03.pzl -text
forge-gui/res/puzzle/PP04.pzl -text
forge-gui/res/puzzle/PP05.pzl -text
forge-gui/res/puzzle/PP06.pzl -text
forge-gui/res/puzzle/PP07.pzl -text
forge-gui/res/puzzle/PP08.pzl -text
forge-gui/res/puzzle/PP09.pzl -text
forge-gui/res/puzzle/PP10.pzl -text
forge-gui/res/puzzle/PP11.pzl -text
forge-gui/res/puzzle/PP12.pzl -text
forge-gui/res/puzzle/PP13.pzl -text
forge-gui/res/puzzle/PP14.pzl -text
forge-gui/res/puzzle/PP15.pzl -text
forge-gui/res/puzzle/PP16.pzl -text
forge-gui/res/puzzle/PP17.pzl -text
forge-gui/res/puzzle/PP18.pzl -text
forge-gui/res/puzzle/PP19.pzl -text
forge-gui/res/puzzle/PP20.pzl -text
forge-gui/res/puzzle/PP22.pzl -text
forge-gui/res/puzzle/PP23.pzl -text
forge-gui/res/puzzle/PP24.pzl -text
forge-gui/res/puzzle/PP25.pzl -text
forge-gui/res/puzzle/PP27.pzl -text
forge-gui/res/puzzle/PP28.pzl -text
forge-gui/res/puzzle/PP29.pzl -text
forge-gui/res/puzzle/PP30.pzl -text
@@ -19437,12 +19543,14 @@ forge-gui/res/puzzle/PS_AER4.pzl -text
forge-gui/res/puzzle/PS_AER5.pzl -text
forge-gui/res/puzzle/PS_AER6.pzl -text
forge-gui/res/puzzle/PS_AER7.pzl -text
forge-gui/res/puzzle/PS_AKH0.pzl -text
forge-gui/res/puzzle/PS_AKH1.pzl -text
forge-gui/res/puzzle/PS_AKH2.pzl -text
forge-gui/res/puzzle/PS_AKH3.pzl -text
forge-gui/res/puzzle/PS_AKH4.pzl -text
forge-gui/res/puzzle/PS_AKH5.pzl -text
forge-gui/res/puzzle/PS_AKH6.pzl -text
forge-gui/res/puzzle/PS_AKH7.pzl -text
forge-gui/res/puzzle/PS_AKH8.pzl -text
forge-gui/res/quest/bazaar/ape_pet_l1.txt -text
forge-gui/res/quest/bazaar/ape_pet_l2.txt -text
@@ -19752,6 +19860,7 @@ forge-gui/res/quest/duels/Hookah-Smoking[!!-~]Caterpillar[!!-~]2.dck -text
forge-gui/res/quest/duels/Hugo[!!-~]Drax[!!-~]1.dck -text
forge-gui/res/quest/duels/Hugo[!!-~]Drax[!!-~]2.dck -text
forge-gui/res/quest/duels/Hulk[!!-~]2.dck -text
forge-gui/res/quest/duels/Ice[!!-~]King[!!-~]1.dck -text
forge-gui/res/quest/duels/Iceman[!!-~]3.dck -text
forge-gui/res/quest/duels/Immortus[!!-~]4.dck -text
forge-gui/res/quest/duels/Imperial[!!-~]Guard[!!-~]2.dck -text
@@ -20818,6 +20927,77 @@ forge-gui/res/quest/world/1997-05[!!-~]Portal/duels/POR[!!-~]3[!!-~]Reanimator.d
forge-gui/res/quest/world/1997-05[!!-~]Portal/duels/POR[!!-~]3[!!-~]Stragegy[!!-~]Red[!!-~]Rock.dck -text
forge-gui/res/quest/world/1997-05[!!-~]Portal/duels/POR[!!-~]3[!!-~]Strategy[!!-~]RDW.dck -text
forge-gui/res/quest/world/1997-05[!!-~]Portal/duels/POR[!!-~]3[!!-~]Strategy[!!-~]Zoo.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Bee[!!-~]Sting.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Chorus[!!-~]of[!!-~]Woe.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Dakmor[!!-~]Plague.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Earthquake.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Foul[!!-~]Spirit.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Goblin[!!-~]War[!!-~]Strike.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Hidden[!!-~]Horror.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Kiss[!!-~]of[!!-~]Death.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Swarm[!!-~]of[!!-~]Rats.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Sylvan[!!-~]Yeti.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Talas[!!-~]Scout.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Challenge[!!-~]Volcanic[!!-~]Hammer.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/challenges/PO2[!!-~]Final[!!-~]Challenge[!!-~]Tojiras[!!-~]Finest.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]1[!!-~]Defenders[!!-~]of[!!-~]Trokin.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]1[!!-~]Heavenly[!!-~]Fury.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]1[!!-~]Protectors[!!-~]of[!!-~]Alaborn.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]2[!!-~]Mystic[!!-~]Masters.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]2[!!-~]Pirates[!!-~]of[!!-~]Talas.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]3[!!-~]Dangers[!!-~]of[!!-~]Dakmor.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]3[!!-~]The[!!-~]Nightstalker[!!-~]Menace.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]3[!!-~]Tojira,[!!-~]Swamp[!!-~]Queen.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]4[!!-~]Earth[!!-~]and[!!-~]Fire.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]4[!!-~]Mountain[!!-~]Giants.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]4[!!-~]The[!!-~]Goblin[!!-~]Horde.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]5[!!-~]The[!!-~]Deep[!!-~]Woods.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]5[!!-~]The[!!-~]Elves[!!-~]of[!!-~]Norwood.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Lore[!!-~]5[!!-~]Wild[!!-~]Beasts.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Starter[!!-~]Set[!!-~]for[!!-~]2[!!-~]Players[!!-~]1[!!-~]BRG.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Starter[!!-~]Set[!!-~]for[!!-~]2[!!-~]Players[!!-~]2[!!-~]GWU.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Theme[!!-~]Deck[!!-~]1[!!-~]Martial[!!-~]Law.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Theme[!!-~]Deck[!!-~]2[!!-~]Spellweaver.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Theme[!!-~]Deck[!!-~]3[!!-~]The[!!-~]Nightstalkers.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Theme[!!-~]Deck[!!-~]4[!!-~]Goblin[!!-~]Fire.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Theme[!!-~]Deck[!!-~]5[!!-~]Natures[!!-~]Assault.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Theme[!!-~]Earth,[!!-~]Wind[!!-~]and[!!-~]Fire.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]1[!!-~]Theme[!!-~]Elementary.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Bighand.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Black[!!-~]Plague.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Breath[!!-~]of[!!-~]Death.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Breath[!!-~]of[!!-~]Fire.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Burn.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Catch[!!-~]Me[!!-~]If[!!-~]You[!!-~]Can.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Deep[!!-~]Breath.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Deep[!!-~]Ramp.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Double[!!-~]Tap.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Giant[!!-~]Ramp.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Goblinpile.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Life[!!-~]is[!!-~]Life.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Nightstalker[!!-~]Lore.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Rat[!!-~]Catcher.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Recoil.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]White[!!-~]Weenie.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Wurmageddon.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Wurmcalling.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]2[!!-~]Gimmick[!!-~]Wurmfire.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Color[!!-~]1[!!-~]White.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Color[!!-~]2[!!-~]Blue.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Color[!!-~]3[!!-~]Black.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Color[!!-~]4[!!-~]Red.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Color[!!-~]5[!!-~]Green.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]01[!!-~]Classic[!!-~]Control.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]02[!!-~]Attrition.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]03[!!-~]Air[!!-~]Superiority.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]04[!!-~]Counterburn.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]05[!!-~]Midrange.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]05[!!-~]Suicide[!!-~]Aggro.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]06[!!-~]Survivors.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]07[!!-~]Wildfire.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]08[!!-~]Swarm[!!-~]Aggro.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]09[!!-~]Weenie[!!-~]Pump.dck -text
forge-gui/res/quest/world/1998-06[!!-~]Portal[!!-~]Second[!!-~]Age/duels/PO2[!!-~]3[!!-~]Strategy[!!-~]10[!!-~]Tempo.dck -text
forge-gui/res/quest/world/Urza/challenges/Ancient.dck -text
forge-gui/res/quest/world/Urza/challenges/Bargain.dck -text
forge-gui/res/quest/world/Urza/challenges/Contamination.dck -text

View File

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

View File

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

View File

@@ -558,7 +558,11 @@ public class AiAttackController {
// TODO probably use AttackConstraints instead of only GlobalAttackRestrictions?
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) {
// can't attack anymore

View File

@@ -151,7 +151,7 @@ public class AiBlockController {
for (final Card c : attackers) {
sortedAttackers.add(c);
}
} else {
} else if (defender instanceof Player && defender.equals(ai)){
firstAttacker = combat.getAttackersOf(defender);
}
}

View File

@@ -1306,7 +1306,7 @@ public class AiController {
+ MyRandom.getRandom().nextInt(3);
return Math.max(remaining, min) / 2;
} 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)) {
return MyRandom.getRandom().nextInt(3);
} else if (source.hasSVar("EnergyToPay")) {
@@ -1428,6 +1428,24 @@ public class AiController {
} else {
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;
}
}
}
}

View File

@@ -192,7 +192,7 @@ public class ComputerUtil {
if (unless != null && !unless.endsWith(">")) {
final int amount = AbilityUtils.calculateAmount(source, unless, sa);
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
final int usableManaSources = ComputerUtilMana.getAvailableMana(ComputerUtil.getOpponentFor(ai), true).size();
// If the Unless isn't enough, this should be less likely to be used
if (amount > usableManaSources) {
@@ -926,7 +926,7 @@ public class ComputerUtil {
return true;
} else if (card.getSVar("PlayMain1").equals("OPPONENTCREATURES")) {
//Only play these main1 when the opponent has creatures (stealing and giving them haste)
if (!card.getController().getOpponent().getCreaturesInPlay().isEmpty()) {
if (!ComputerUtil.getOpponentFor(card.getController()).getCreaturesInPlay().isEmpty()) {
return true;
}
} else if (!card.getController().getCreaturesInPlay().isEmpty()) {
@@ -973,7 +973,7 @@ public class ComputerUtil {
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;
}
if (card.isCreature()) {
@@ -993,7 +993,7 @@ public class ComputerUtil {
} // BuffedBy
// 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) {
if (buffedcard.hasSVar("AntiBuffedBy")) {
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
@@ -1033,11 +1033,11 @@ public class ComputerUtil {
return ret;
} else {
// Otherwise, if life is possibly in danger, then this is fine.
Combat combat = new Combat(ai.getOpponent());
CardCollectionView attackers = ai.getOpponent().getCreaturesInPlay();
Combat combat = new Combat(ComputerUtil.getOpponentFor(ai));
CardCollectionView attackers = ComputerUtil.getOpponentFor(ai).getCreaturesInPlay();
for (Card att : attackers) {
if (ComputerUtilCombat.canAttackNextTurn(att, ai)) {
combat.addAttacker(att, att.getController().getOpponent());
combat.addAttacker(att, ComputerUtil.getOpponentFor(att.getController()));
}
}
AiBlockController aiBlock = new AiBlockController(ai);
@@ -1101,7 +1101,7 @@ public class ComputerUtil {
return (sa.getHostCard().isCreature()
&& sa.getPayCosts().hasTapCost()
&& (ph.getPhase().isBefore(PhaseType.COMBAT_DECLARE_BLOCKERS)
|| !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !ph.getNextTurn().equals(sa.getActivatingPlayer()))
&& !sa.getHostCard().hasSVar("EndOfTurnLeavePlay")
&& !sa.hasParam("ActivationPhases"));
}
@@ -1145,7 +1145,7 @@ public class ComputerUtil {
}
// 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) {
if (buffedcard.hasSVar("AntiBuffedBy")) {
final String buffedby = buffedcard.getSVar("AntiBuffedBy");
@@ -1331,7 +1331,7 @@ public class ComputerUtil {
if (tgt == null) {
continue;
}
final Player enemy = ai.getOpponent();
final Player enemy = ComputerUtil.getOpponentFor(ai);
if (!sa.canTarget(enemy)) {
continue;
}
@@ -2047,7 +2047,7 @@ public class ComputerUtil {
}
}
else if (logic.equals("ChosenLandwalk")) {
for (Card c : ai.getOpponent().getLandsInPlay()) {
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
for (String t : c.getType()) {
if (!invalidTypes.contains(t) && CardType.isABasicLandType(t)) {
chosen = t;
@@ -2065,7 +2065,7 @@ public class ComputerUtil {
else if (kindOfType.equals("Land")) {
if (logic != null) {
if (logic.equals("ChosenLandwalk")) {
for (Card c : ai.getOpponent().getLandsInPlay()) {
for (Card c : ComputerUtil.getOpponentFor(ai).getLandsInPlay()) {
for (String t : c.getType().getLandTypes()) {
if (!invalidTypes.contains(t)) {
chosen = t;
@@ -2098,7 +2098,7 @@ public class ComputerUtil {
case "Torture":
return "Torture";
case "GraceOrCondemnation":
return ai.getCreaturesInPlay().size() > ai.getOpponent().getCreaturesInPlay().size() ? "Grace"
return ai.getCreaturesInPlay().size() > ComputerUtil.getOpponentFor(ai).getCreaturesInPlay().size() ? "Grace"
: "Condemnation";
case "CarnageOrHomage":
CardCollection cardsInPlay = CardLists
@@ -2683,4 +2683,23 @@ public class ComputerUtil {
}
return true;
}
public static final Player getOpponentFor(final Player player) {
Player opponent = null;
int minLife = Integer.MAX_VALUE;
for (Player p : player.getOpponents()) {
if (p.getLife() < minLife) {
opponent = p;
minLife = p.getLife();
}
}
if (opponent != null) {
return opponent;
}
throw new IllegalStateException("No opponents left ingame for " + player);
}
}

View File

@@ -482,7 +482,7 @@ public class ComputerUtilCard {
*/
public static CardCollectionView getLikelyBlockers(final Player ai, final CardCollectionView blockers) {
AiBlockController aiBlk = new AiBlockController(ai);
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
Combat combat = new Combat(opp);
//Use actual attackers if available, else consider all possible attackers
Combat currentCombat = ai.getGame().getCombat();
@@ -845,9 +845,10 @@ public class ComputerUtilCard {
List<String> chosen = new ArrayList<String>();
Player ai = sa.getActivatingPlayer();
final Game game = ai.getGame();
Player opp = ai.getOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.hasParam("AILogic")) {
final String logic = sa.getParam("AILogic");
if (logic.equals("MostProminentInHumanDeck")) {
chosen.add(ComputerUtilCard.getMostProminentColor(CardLists.filterControlledBy(game.getCardsInGame(), opp), colorChoices));
}
@@ -873,7 +874,7 @@ public class ComputerUtilCard {
chosen.add(ComputerUtilCard.getMostProminentColor(ai.getCardsIn(ZoneType.Battlefield), colorChoices));
}
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")) {
chosen.add(ComputerUtilCard.getMostProminentColor(game.getCardsIn(ZoneType.Battlefield), colorChoices));
@@ -897,7 +898,7 @@ public class ComputerUtilCard {
String bestColor = Constant.GREEN;
for (byte color : MagicColor.WUBRG) {
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));
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));
@@ -934,7 +935,7 @@ public class ComputerUtilCard {
public static boolean useRemovalNow(final SpellAbility sa, final Card c, final int dmg, ZoneType destination) {
final Player ai = sa.getActivatingPlayer();
final AiController aic = ((PlayerControllerAi)ai.getController()).getAi();
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
final Game game = ai.getGame();
final PhaseHandler ph = game.getPhaseHandler();
final PhaseType phaseType = ph.getPhase();
@@ -1217,7 +1218,7 @@ public class ComputerUtilCard {
}
}
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
Card pumped = getPumpedCreature(ai, sa, c, toughness, power, keywords);
List<Card> oppCreatures = opp.getCreaturesInPlay();
float chance = 0;

View File

@@ -1308,6 +1308,10 @@ public class ComputerUtilCombat {
bonus = bonus.replace("TriggerCount$NumBlockers", "Number$1");
} else if (bonus.contains("TriggeredPlayersDefenders$Amount")) { // for Melee
bonus = bonus.replace("TriggeredPlayersDefenders$Amount", "Number$1");
} else if (bonus.contains("TriggeredAttacker$CardPower")) { // e.g. Arahbo, Roar of the World
bonus = bonus.replace("TriggeredAttacker$CardPower", "Number$" + attacker.getNetPower());
} else if (bonus.contains("TriggeredAttacker$CardToughness")) {
bonus = bonus.replace("TriggeredAttacker$CardToughness", "Number$" + attacker.getNetToughness());
}
power += CardFactoryUtil.xCount(source, bonus);
@@ -1741,6 +1745,11 @@ public class ComputerUtilCombat {
// consider Damage Prevention/Replacement
defenderDamage = predictDamageTo(attacker, defenderDamage, possibleAttackerPrevention, blocker, 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)
+ ComputerUtilCombat.predictToughnessBonusOfBlocker(attacker, blocker, withoutAbilities);
@@ -2431,6 +2440,20 @@ public class ComputerUtilCombat {
int afflictDmg = attacker.getKeywordMagnitude("Afflict");
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;
}
}

View File

@@ -566,7 +566,7 @@ public class ComputerUtilCost {
&& (!source.getName().equals("Tyrannize") || payer.getCardsIn(ZoneType.Hand).size() > 2)
&& (!source.getName().equals("Perplex") || payer.getCardsIn(ZoneType.Hand).size() < 2)
&& (!source.getName().equals("Breaking Point") || payer.getCreaturesInPlay().size() > 1)
&& (!source.getName().equals("Chain of Vapor") || (payer.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) {

View File

@@ -219,25 +219,32 @@ public class ComputerUtilMana {
}
}
SpellAbility paymentChoice = ma;
// Exception: when paying generic mana with Cavern of Souls, prefer the colored mana producing ability
// to attempt to make the spell uncounterable when possible.
if ((toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X)
&& ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
if (ComputerUtilAbility.getAbilitySourceName(ma).equals("Cavern of Souls")
&& sa.getHostCard().getType().getCreatureTypes().contains(ma.getHostCard().getChosenType())) {
for (SpellAbility ab : saList) {
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
return ab;
if (toPay == ManaCostShard.COLORLESS && cost.getUnpaidShards().contains(ManaCostShard.GENERIC)) {
// Deprioritize Cavern of Souls, try to pay generic mana with it instead to use the NoCounter ability
continue;
} else if (toPay == ManaCostShard.GENERIC || toPay == ManaCostShard.X) {
for (SpellAbility ab : saList) {
if (ab.isManaAbility() && ab.getManaPart().isAnyMana() && ab.hasParam("AddsNoCounter")) {
paymentChoice = ab;
break;
}
}
}
}
final String typeRes = cost.getSourceRestriction();
if (StringUtils.isNotBlank(typeRes) && !ma.getHostCard().getType().hasStringType(typeRes)) {
if (StringUtils.isNotBlank(typeRes) && !paymentChoice.getHostCard().getType().hasStringType(typeRes)) {
continue;
}
if (canPayShardWithSpellAbility(toPay, ai, ma, sa, checkCosts)) {
return ma;
if (canPayShardWithSpellAbility(toPay, ai, paymentChoice, sa, checkCosts)) {
return paymentChoice;
}
}
return null;

View File

@@ -4,15 +4,21 @@ import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.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.game.Game;
import forge.game.GameEntity;
@@ -23,14 +29,21 @@ import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactory;
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.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
import forge.item.IPaperCard;
import forge.item.PaperCard;
import forge.util.TextUtil;
import forge.util.collect.FCollectionView;
import org.apache.commons.lang3.StringUtils;
public abstract class GameState {
private static final Map<ZoneType, String> ZONES = new HashMap<ZoneType, String>();
@@ -53,12 +66,30 @@ public abstract class GameState {
private final Map<Integer, Card> idToCard = 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, 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 Set<Card> cardsReferencedByID = new HashSet<>();
private String tChangePlayer = "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() {
}
@@ -110,6 +141,40 @@ public abstract class GameState {
tChangePhase = game.getPhaseHandler().getPhase().toString();
aiCardTexts.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
// TODO: we can currently support remembered cards only. Expand to support other remembered objects.
if (o instanceof Card) {
cardsReferencedByID.add((Card)o);
}
}
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()) {
// Init texts to empty, so that restoring will clear the state
// if the zone had no cards in it (e.g. empty hand).
@@ -140,6 +205,11 @@ public abstract class GameState {
if (c.isCommander()) {
newText.append("|IsCommander");
}
if (cardsReferencedByID.contains(c)) {
newText.append("|Id:").append(c.getId());
}
if (zoneType == ZoneType.Battlefield) {
if (c.isTapped()) {
newText.append("|Tapped");
@@ -147,6 +217,16 @@ public abstract class GameState {
if (c.isSick()) {
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()) {
newText.append("|FaceDown");
if (c.isManifested()) {
@@ -155,6 +235,10 @@ public abstract class GameState {
}
if (c.getCurrentStateName().equals(CardStateName.Transformed)) {
newText.append("|Transformed");
} else if (c.getCurrentStateName().equals(CardStateName.Flipped)) {
newText.append("|Flipped");
} else if (c.getCurrentStateName().equals(CardStateName.Meld)) {
newText.append("|Meld");
}
Map<CounterType, Integer> counters = c.getCounters();
if (!counters.isEmpty()) {
@@ -169,10 +253,45 @@ public abstract class GameState {
newText.append("|Attaching:").append(c.getEnchantingCard().getId());
}
if (!c.getEnchantedBy(false).isEmpty() || !c.getEquippedBy(false).isEmpty() || !c.getFortifiedBy(false).isEmpty()) {
newText.append("|Id:").append(c.getId());
if (c.getDamage() > 0) {
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, ","));
}
}
if (zoneType == ZoneType.Exile) {
if (c.getExiledWith() != null) {
newText.append("|ExiledWith:").append(c.getExiledWith().getId());
}
}
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());
}
@@ -298,6 +417,12 @@ public abstract class GameState {
abilityString.put(categoryName.substring("ability".length()), categoryValue);
}
else if (categoryName.endsWith("precast")) {
if (isHuman)
precastHuman = categoryValue;
else
precastAI = categoryValue;
}
else {
System.out.println("Unknown key: " + categoryName);
}
@@ -318,6 +443,13 @@ public abstract class GameState {
idToCard.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;
PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase);
@@ -331,20 +463,300 @@ public abstract class GameState {
if (!computerCounters.isEmpty()) {
applyCountersToGameEntity(ai, computerCounters);
}
game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn);
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
game.getTriggerHandler().setSuppressAllTriggers(true);
setupPlayerState(humanLife, humanCardTexts, human);
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.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);
}
}
// 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.indexOf("->") + 2);
tgtID = parseTargetInScript(tgtDef);
sPtr = sPtr.substring(0, sPtr.indexOf("->"));
}
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 {
if (!c.hasSVar(sPtr)) {
System.err.println("ERROR: Unable to find SVar " + sPtr + " on card " + c + " + to execute!");
return;
}
String svarValue = c.getSVar(sPtr);
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();
}
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) {
//entity.setCounters(new HashMap<CounterType, Integer>());
String[] allCounterStrings = counterString.split(",");
@@ -356,7 +768,6 @@ public abstract class GameState {
private void setupPlayerState(int life, Map<ZoneType, String> cardTexts, final Player p) {
// Lock check static as we setup player state
final Game game = p.getGame();
Map<ZoneType, CardCollectionView> playerCards = new EnumMap<ZoneType, CardCollectionView>(ZoneType.class);
for (Entry<ZoneType, String> kv : cardTexts.entrySet()) {
@@ -384,9 +795,13 @@ public abstract class GameState {
Map<CounterType, Integer> counters = c.getCounters();
// Note: Not clearCounters() since we want to keep the counters
// var as-is.
c.setCounters(new HashMap<CounterType, Integer>());
c.setCounters(Maps.<CounterType, Integer>newEnumMap(CounterType.class));
p.getZone(ZoneType.Hand).add(c);
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);
} else {
p.getGame().getAction().moveToPlay(c, null);
@@ -401,25 +816,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 +849,11 @@ public abstract class GameState {
c = CardFactory.makeOneToken(CardFactory.TokenInfo.fromString(tokenStr), player);
} else {
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);
if (setCode != null) {
hasSetCurSet = true;
@@ -463,6 +864,13 @@ public abstract class GameState {
for (final String info : cardinfo) {
if (info.startsWith("Tapped")) {
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:")) {
applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1));
} else if (info.startsWith("SummonSick")) {
@@ -474,6 +882,10 @@ public abstract class GameState {
}
} else if (info.startsWith("Transformed")) {
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")) {
// TODO: This doesn't seem to properly restore the ability to play the commander. Why?
c.setCommander(true);
@@ -488,6 +900,26 @@ public abstract class GameState {
} else if (info.startsWith("Ability:")) {
String abString = info.substring(info.indexOf(':') + 1).toLowerCase();
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("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 forge.AIOption;
import forge.LobbyPlayer;
import forge.game.Game;
import forge.game.player.IGameEntitiesFactory;

View File

@@ -3,6 +3,7 @@ package forge.ai;
import java.security.InvalidParameterException;
import java.util.*;
import forge.card.CardStateName;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.ImmutablePair;
import org.apache.commons.lang3.tuple.Pair;
@@ -46,7 +47,6 @@ import forge.game.player.PlayerController;
import forge.game.player.PlayerView;
import forge.game.replacement.ReplacementEffect;
import forge.game.spellability.*;
import forge.game.trigger.Trigger;
import forge.game.trigger.WrappedAbility;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
@@ -115,7 +115,23 @@ public class PlayerControllerAi extends PlayerController {
if (ability.getApi() != null) {
switch (ability.getApi()) {
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:
return 0;
default:
@@ -179,8 +195,8 @@ public class PlayerControllerAi extends PlayerController {
@Override
public boolean confirmTrigger(WrappedAbility wrapper, Map<String, String> triggerParams, boolean isMandatory) {
final SpellAbility sa = wrapper.getWrappedAbility();
final Trigger regtrig = wrapper.getTrigger();
final SpellAbility sa = wrapper.getWrappedAbility();
//final Trigger regtrig = wrapper.getTrigger();
if (ComputerUtilAbility.getAbilitySourceName(sa).equals("Deathmist Raptor")) {
return true;
}
@@ -844,20 +860,37 @@ public class PlayerControllerAi extends PlayerController {
@Override
public String chooseCardName(SpellAbility sa, Predicate<ICardFace> cpp, String valid, String message) {
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");
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")) {
return ComputerUtilCard.getMostProminentCardName(player.getCardsIn(ZoneType.Library));
return ComputerUtilCard.getMostProminentCardName(aiLibrary);
} else if (logic.equals("MostProminentInHumanDeck")) {
return ComputerUtilCard.getMostProminentCardName(player.getOpponent().getCardsIn(ZoneType.Library));
return ComputerUtilCard.getMostProminentCardName(oppLibrary);
} 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);
} else if (logic.equals("BestCreatureInComputerDeck")) {
return ComputerUtilCard.getBestCreatureAI(player.getCardsIn(ZoneType.Library)).getName();
return ComputerUtilCard.getBestCreatureAI(aiLibrary).getName();
} else if (logic.equals("RandomInComputerDeck")) {
return Aggregates.random(player.getCardsIn(ZoneType.Library)).getName();
return Aggregates.random(aiLibrary).getName();
} 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);
}
} else {

View File

@@ -17,28 +17,17 @@
*/
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.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.card.mana.ManaCost;
import forge.game.Game;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardFactoryUtil;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CounterType;
import forge.game.card.*;
import forge.game.cost.CostPart;
import forge.game.mana.ManaCostBeingPaid;
import forge.game.phase.PhaseHandler;
@@ -49,6 +38,10 @@ import forge.game.spellability.SpellAbility;
import forge.game.zone.ZoneType;
import forge.util.Aggregates;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Special logic for individual cards
*
@@ -139,7 +132,23 @@ public class SpecialCardAi {
return chosen;
}
}
// Chain of Acid
public static class ChainOfAcid {
public static boolean consider(Player ai, 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
public static class ChainOfSmog {
public static boolean consider(Player ai, SpellAbility sa) {
@@ -351,7 +360,35 @@ public class SpecialCardAi {
return (aiGraveyardPower - aiBattlefieldPower) > (oppGraveyardPower - oppBattlefieldPower);
}
}
// 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(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;
}
}
// Necropotence
public static class Necropotence {
public static boolean consider(Player ai, SpellAbility sa) {

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import forge.ai.AiCardMemory;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
@@ -86,13 +86,13 @@ public class AnimateAi extends SpellAbilityAi {
num = (num == null) ? "1" : num;
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
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));
ComputerUtilCard.sortByEvaluateCreature(list);
if (!list.isEmpty() && list.size() == nToSac && ComputerUtilCost.canPayCost(sa, ai)) {
Card animatedCopy = becomeAnimated(source, sa);
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);
list = CardLists.filter(list, CardPredicates.canBeSacrificedBy(topStack));
if (ComputerUtilCard.evaluateCreature(animatedCopy) < ComputerUtilCard.evaluateCreature(list.get(0))

View File

@@ -580,8 +580,8 @@ public class AttachAi extends SpellAbilityAi {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
String kws = stabMap.get("AddKeyword");
if (kws != null) {
@@ -702,30 +702,6 @@ public class AttachAi extends SpellAbilityAi {
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.
*
@@ -874,8 +850,8 @@ public class AttachAi extends SpellAbilityAi {
continue;
}
if ((affected.contains(stCheck) || affected.contains("AttachedBy"))) {
totToughness += AttachAi.parseSVar(attachSource, stabMap.get("AddToughness"));
totPower += AttachAi.parseSVar(attachSource, stabMap.get("AddPower"));
totToughness += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddToughness"), sa);
totPower += AbilityUtils.calculateAmount(attachSource, stabMap.get("AddPower"), sa);
grantingAbilities |= stabMap.containsKey("AddAbility");

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.SpellAbilityAi;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
@@ -16,7 +17,7 @@ public class BalanceAi extends SpellAbilityAi {
int diff = 0;
// 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 compPerms = aiPlayer.getCardsIn(ZoneType.Battlefield);

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -24,7 +25,7 @@ public class BidLifeAi extends SpellAbilityAi {
if (tgt != null) {
sa.resetTargets();
if (tgt.canTgtCreature()) {
List<Card> list = CardLists.getTargetableCards(aiPlayer.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);
if (list.isEmpty()) {
return false;

View File

@@ -7,6 +7,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Map;
import forge.game.card.*;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
@@ -22,11 +23,6 @@ import forge.game.GameObject;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardCollectionView;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates;
import forge.game.card.CardPredicates.Presets;
import forge.game.combat.Combat;
import forge.game.cost.Cost;
@@ -166,7 +162,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
final String sourceName = ComputerUtilAbility.getAbilitySourceName(sa);
ZoneType origin = null;
final Player opponent = ai.getOpponent();
final Player opponent = ComputerUtil.getOpponentFor(ai);
boolean activateForCost = ComputerUtil.activateForCost(sa, ai);
if (sa.hasParam("Origin")) {
@@ -367,7 +363,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
// if putting cards from hand to library and parent is drawing cards
// make sure this will actually do something:
final TargetRestrictions tgt = sa.getTargetRestrictions();
final Player opp = aiPlayer.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(aiPlayer);
if (tgt != null && tgt.canTgtPlayer()) {
boolean isCurse = sa.isCurse();
if (isCurse && sa.canTarget(opp)) {
@@ -428,7 +424,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
Iterable<Player> pDefined;
final TargetRestrictions tgt = sa.getTargetRestrictions();
if ((tgt != null) && tgt.canTgtPlayer()) {
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.isCurse()) {
if (sa.canTarget(opp)) {
sa.getTargets().add(opp);
@@ -547,8 +543,9 @@ public class ChangeZoneAi extends SpellAbilityAi {
*/
private static Card chooseCreature(final Player ai, CardCollection list) {
// Creating a new combat for testing purposes.
Combat combat = new Combat(ai.getOpponent());
for (Card att : ai.getOpponent().getCreaturesInPlay()) {
final Player opponent = ComputerUtil.getOpponentFor(ai);
Combat combat = new Combat(opponent);
for (Card att : opponent.getCreaturesInPlay()) {
combat.addAttacker(att, ai);
}
AiBlockController block = new AiBlockController(ai);
@@ -647,6 +644,20 @@ public class ChangeZoneAi extends SpellAbilityAi {
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();
@@ -828,7 +839,7 @@ public class ChangeZoneAi extends SpellAbilityAi {
&& !currCombat.getBlockers(attacker).isEmpty()) {
ComputerUtilCard.sortByEvaluateCreature(blockers);
Combat combat = new Combat(ai);
combat.addAttacker(attacker, ai.getOpponent());
combat.addAttacker(attacker, ComputerUtil.getOpponentFor(ai));
for (Card blocker : blockers) {
combat.addBlocker(attacker, blocker);
}
@@ -1319,6 +1330,8 @@ public class ChangeZoneAi extends SpellAbilityAi {
}
} else if ("WorstCard".equals(logic)) {
return ComputerUtilCard.getWorstAI(fetchList);
} else if ("Mairsil".equals(logic)) {
return SpecialCardAi.MairsilThePretender.considerCardFromList(fetchList);
}
}
if (fetchList.isEmpty()) {

View File

@@ -73,14 +73,15 @@ public class ChangeZoneAllAi extends SpellAbilityAi {
oppType = AbilityUtils.filterListByType(oppType, sa.getParam("ChangeType"), sa);
computerType = AbilityUtils.filterListByType(computerType, sa.getParam("ChangeType"), sa);
// Living Death AI
if ("LivingDeath".equals(sa.getParam("AILogic"))) {
// Living Death AI
return SpecialCardAi.LivingDeath.consider(ai, sa);
}
// Timetwister AI
if ("Timetwister".equals(sa.getParam("AILogic"))) {
} else if ("Timetwister".equals(sa.getParam("AILogic"))) {
// Timetwister AI
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);
}
// TODO improve restrictions on when the AI would want to use this

View File

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

View File

@@ -58,7 +58,7 @@ public class ChooseCardNameAi extends SpellAbilityAi {
if (tgt != null) {
sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(ai.getOpponent());
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} else {
sa.getTargets().add(ai);
}

View File

@@ -1,6 +1,5 @@
package forge.ai.ability;
import com.google.common.base.Predicates;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import forge.ai.ComputerUtilCard;
@@ -53,7 +52,7 @@ public class ChooseColorAi extends SpellAbilityAi {
}
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 true;
@@ -62,7 +61,7 @@ public class ChooseColorAi extends SpellAbilityAi {
if (logic.equals("MostExcessOpponentControls")) {
for (byte color : MagicColor.WUBRG) {
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));
opplist = CardLists.filter(opplist, CardPredicates.isColor(color));

View File

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

View File

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

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
@@ -29,7 +30,7 @@ public class ControlExchangeAi extends SpellAbilityAi {
sa.resetTargets();
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
// purpose
list = CardLists.filter(list, new Predicate<Card>() {

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import forge.ai.SpecialCardAi;
import forge.ai.SpellAbilityAi;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import java.util.List;
@@ -25,9 +26,10 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
public boolean chkAIDrawback(final SpellAbility sa, final Player aiPlayer) {
// NOTE: Other SAs that use CopySpellAbilityAi (e.g. Chain Lightning) are currently routed through
// generic method SpellAbilityAi#chkDrawbackWithSubs and are handled there.
if ("ChainOfSmog".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfSmog.consider(aiPlayer, sa);
} else if ("ChainOfAcid".equals(sa.getParam("AILogic"))) {
return SpecialCardAi.ChainOfAcid.consider(aiPlayer, sa);
}
return super.chkAIDrawback(sa, aiPlayer);
@@ -37,5 +39,17 @@ public class CopySpellAbilityAi extends SpellAbilityAi {
public SpellAbility chooseSingleSpellAbility(Player player, SpellAbility sa, List<SpellAbility> spells) {
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

@@ -2,6 +2,7 @@ package forge.ai.ability;
import forge.ai.AiController;
import forge.ai.AiProps;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilAbility;
import java.util.Iterator;
@@ -94,7 +95,7 @@ public class CounterAi extends SpellAbilityAi {
if (unlessCost != null && !unlessCost.endsWith(">")) {
// Is this Usable Mana Sources? Or Total Available Mana?
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
final int usableManaSources = ComputerUtilMana.getAvailableMana(ComputerUtil.getOpponentFor(ai), true).size();
int toPay = 0;
boolean setPayX = false;
if (unlessCost.equals("X") && source.getSVar(unlessCost).equals("Count$xPaid")) {
@@ -213,7 +214,7 @@ public class CounterAi extends SpellAbilityAi {
final Card source = sa.getHostCard();
if (unlessCost != null) {
// Is this Usable Mana Sources? Or Total Available Mana?
final int usableManaSources = ComputerUtilMana.getAvailableMana(ai.getOpponent(), true).size();
final int usableManaSources = ComputerUtilMana.getAvailableMana(ComputerUtil.getOpponentFor(ai), true).size();
int toPay = 0;
boolean setPayX = false;
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.CostPart;
import forge.game.cost.CostRemoveCounter;
import forge.game.cost.CostSacrifice;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
@@ -373,6 +374,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)) {
return false;
}

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
@@ -35,10 +37,11 @@ public class CountersPutAllAi extends SpellAbilityAi {
final String type = sa.getParam("CounterType");
final String amountStr = sa.getParam("CounterNum");
final String valid = sa.getParam("ValidCards");
final String logic = sa.getParamOrDefault("AILogic", "");
final boolean curse = sa.isCurse();
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);
if (abCost != null) {
@@ -56,8 +59,14 @@ public class CountersPutAllAi extends SpellAbilityAi {
}
}
if (logic.equals("AtEOTOrBlock")) {
if (!ai.getGame().getPhaseHandler().is(PhaseType.END_OF_TURN) && !ai.getGame().getPhaseHandler().is(PhaseType.COMBAT_DECLARE_BLOCKERS)) {
return false;
}
}
if (tgt != null) {
Player pl = curse ? ai.getOpponent() : ai;
Player pl = curse ? ComputerUtil.getOpponentFor(ai) : ai;
sa.getTargets().add(pl);
hList = CardLists.filterControlledBy(hList, pl);
@@ -138,6 +147,6 @@ public class CountersPutAllAi extends SpellAbilityAi {
*/
@Override
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

@@ -2,6 +2,7 @@ package forge.ai.ability;
import com.google.common.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -18,7 +19,7 @@ public abstract class DamageAiBase extends SpellAbilityAi {
protected boolean shouldTgtP(final Player comp, final SpellAbility sa, final int d, final boolean noPrevention) {
int restDamage = d;
final Game game = comp.getGame();
final Player enemy = comp.getOpponent();
final Player enemy = ComputerUtil.getOpponentFor(comp);
if (!sa.canTarget(enemy)) {
return false;
}

View File

@@ -59,7 +59,7 @@ public class DamageAllAi extends SpellAbilityAi {
return evaluateDamageAll(ai, sa, source, dmg) > 0;
} else {
int best = -1, best_x = -1;
for (int i = 0; i < x; i++) {
for (int i = 0; i <= x; i++) {
final int value = evaluateDamageAll(ai, sa, source, i);
if (value > best) {
best = value;
@@ -80,7 +80,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
private int evaluateDamageAll(Player ai, SpellAbility sa, final Card source, int dmg) {
Player opp = ai.getOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
final CardCollection humanList = getKillableCreatures(sa, opp, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
@@ -179,7 +179,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
// Evaluate creatures getting killed
Player enemy = ai.getOpponent();
Player enemy = ComputerUtil.getOpponentFor(ai);
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
final TargetRestrictions tgt = sa.getTargetRestrictions();
@@ -261,7 +261,7 @@ public class DamageAllAi extends SpellAbilityAi {
}
// Evaluate creatures getting killed
Player enemy = ai.getOpponent();
Player enemy = ComputerUtil.getOpponentFor(ai);
final CardCollection humanList = getKillableCreatures(sa, enemy, dmg);
CardCollection computerList = getKillableCreatures(sa, ai, dmg);
final TargetRestrictions tgt = sa.getTargetRestrictions();

View File

@@ -345,7 +345,7 @@ public class DamageDealAi extends DamageAiBase {
final boolean divided = sa.hasParam("DividedAsYouChoose");
final boolean oppTargetsChoice = sa.hasParam("TargetingPlayer");
Player enemy = ai.getOpponent();
Player enemy = ComputerUtil.getOpponentFor(ai);
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()
@@ -377,7 +377,7 @@ public class DamageDealAi extends DamageAiBase {
}
if ("Polukranos".equals(sa.getParam("AILogic"))) {
int dmgTaken = 0;
CardCollection humCreatures = ai.getOpponent().getCreaturesInPlay();
CardCollection humCreatures = enemy.getCreaturesInPlay();
Card lastTgt = null;
humCreatures = CardLists.getTargetableCards(humCreatures, sa);
ComputerUtilCard.sortByEvaluateCreature(humCreatures);
@@ -633,7 +633,7 @@ public class DamageDealAi extends DamageAiBase {
// this is for Triggered targets that are mandatory
final boolean noPrevention = sa.hasParam("NoPrevention");
final boolean divided = sa.hasParam("DividedAsYouChoose");
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
System.out.println("damageChooseRequiredTargets " + ai + " " + 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.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.PlayerCollection;
import forge.game.player.PlayerPredicates;

View File

@@ -4,6 +4,7 @@ import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.SpellAbilityAi;
@@ -176,7 +177,7 @@ public class DebuffAi extends SpellAbilityAi {
* @return a CardCollection.
*/
private CardCollection getCurseCreatures(final Player ai, final SpellAbility sa, final List<String> kws) {
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
CardCollection list = CardLists.getTargetableCards(opp.getCreaturesInPlay(), sa);
if (!list.isEmpty()) {
list = CardLists.filter(list, new Predicate<Card>() {
@@ -216,7 +217,7 @@ public class DebuffAi extends SpellAbilityAi {
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 Card source = sa.getHostCard();

View File

@@ -267,7 +267,7 @@ public class DestroyAi extends SpellAbilityAi {
} else if (sa.hasParam("Defined")) {
list = AbilityUtils.getDefinedCards(source, sa.getParam("Defined"), sa);
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)
|| ai.getLife() <= 5)) {
// Basic ai logic for Lethal Vapors

View File

@@ -64,7 +64,7 @@ public class DestroyAllAi extends SpellAbilityAi {
public boolean doMassRemovalLogic(Player ai, SpellAbility sa) {
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;

View File

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

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilMana;
import forge.ai.SpellAbilityAi;
import forge.game.card.Card;
@@ -29,7 +30,7 @@ public class DigUntilAi extends SpellAbilityAi {
final boolean randomReturn = r.nextFloat() <= Math.pow(chance, sa.getActivationsThisTurn() + 1);
Player libraryOwner = ai;
Player opp = ai.getOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if (sa.usesTargeting()) {
sa.resetTargets();

View File

@@ -53,7 +53,7 @@ public class DiscardAi extends SpellAbilityAi {
return MyRandom.getRandom().nextFloat() < (1.0 / (1 + hand));
}
final boolean humanHasHand = ai.getOpponent().getCardsIn(ZoneType.Hand).size() > 0;
final boolean humanHasHand = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Hand).size() > 0;
if (tgt != null) {
if (!discardTargetAI(ai, sa)) {
@@ -84,7 +84,7 @@ public class DiscardAi extends SpellAbilityAi {
if (sa.hasParam("NumCards")) {
if (sa.getParam("NumCards").equals("X") && source.getSVar("X").equals("Count$xPaid")) {
// 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());
if (cardsToDiscard < 1) {
return false;
@@ -120,7 +120,7 @@ public class DiscardAi extends SpellAbilityAi {
private boolean discardTargetAI(final Player ai, final SpellAbility sa) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
Player opp = ai.getOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if (opp.getCardsIn(ZoneType.Hand).isEmpty() && !ComputerUtil.activateForCost(sa, ai)) {
return false;
}
@@ -139,7 +139,7 @@ public class DiscardAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) {
Player opp = ai.getOpponent();
Player opp = ComputerUtil.getOpponentFor(ai);
if (!discardTargetAI(ai, sa)) {
if (mandatory && sa.canTarget(opp)) {
sa.getTargets().add(opp);
@@ -160,7 +160,7 @@ public class DiscardAi extends SpellAbilityAi {
}
if ("X".equals(sa.getParam("RevealNumber")) && sa.getHostCard().getSVar("X").equals("Count$xPaid")) {
// 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());
sa.getHostCard().setSVar("PayX", Integer.toString(cardsToDiscard));
}

View File

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

View File

@@ -16,6 +16,7 @@ import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.ability.ApiType;
import forge.game.card.Card;
import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.combat.CombatUtil;
import forge.game.phase.PhaseHandler;
@@ -179,9 +180,7 @@ public class EffectAi extends SpellAbilityAi {
break;
}
if (shouldPlay) {
return true;
}
return shouldPlay;
} else if (logic.equals("RedirectSpellDamageFromPlayer")) {
if (game.getStack().isEmpty()) {
return false;
@@ -246,6 +245,12 @@ public class EffectAi extends SpellAbilityAi {
}
}
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
return false;

View File

@@ -1,6 +1,7 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -58,7 +59,7 @@ public class FogAi extends SpellAbilityAi {
protected boolean doTriggerAINoCost(Player aiPlayer, SpellAbility sa, boolean mandatory) {
final Game game = aiPlayer.getGame();
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);
} else {
chance = game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_DAMAGE);

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
@@ -60,7 +62,7 @@ public class MustBlockAi extends SpellAbilityAi {
boolean chance = false;
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.getValidCards(list, abTgt.getValidTgts(), source.getController(), source, sa);
list = CardLists.filter(list, new Predicate<Card>() {

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)
*/
@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) {
List<Card> tgtCards = CardLists.filter(options, new Predicate<Card>() {
@Override

View File

@@ -2,6 +2,7 @@ package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.game.ability.AbilityUtils;
@@ -31,7 +32,7 @@ public class PowerExchangeAi extends SpellAbilityAi {
sa.resetTargets();
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
// purpose
list = CardLists.filter(list, new Predicate<Card>() {

View File

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

View File

@@ -3,6 +3,7 @@ package forge.ai.ability;
import java.util.Arrays;
import java.util.List;
import com.google.common.base.Predicates;
import org.apache.commons.lang3.StringUtils;
import com.google.common.base.Predicate;
@@ -461,6 +462,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 (ComputerUtil.activateForCost(sa, ai)) {
return pumpMandatoryTarget(ai, sa);

View File

@@ -181,7 +181,7 @@ public abstract class PumpAiBase extends SpellAbilityAi {
final Game game = ai.getGame();
final Combat combat = game.getCombat();
final PhaseHandler ph = game.getPhaseHandler();
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
final int newPower = card.getNetCombatDamage() + attack;
//int defense = getNumDefense(sa);
if (!CardUtil.isStackingKeyword(keyword) && card.hasKeyword(keyword)) {

View File

@@ -57,7 +57,7 @@ public class PumpAllAi extends PumpAiBase {
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 human = CardLists.getValidCards(opp.getCardsIn(ZoneType.Battlefield), valid, source.getController(), source);

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,6 @@
package forge.ai.ability;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
import forge.ai.ComputerUtilMana;
@@ -38,7 +39,7 @@ public class SacrificeAllAi extends SpellAbilityAi {
}
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 =
CardLists.getValidCards(ai.getCardsIn(ZoneType.Battlefield), valid.split(","), source.getController(), source, sa);

View File

@@ -1,19 +1,14 @@
package forge.ai.ability;
import com.google.common.base.Predicate;
import forge.ai.ComputerUtilCard;
import forge.ai.SpellAbilityAi;
import forge.card.CardSplitType;
import forge.card.CardStateName;
import forge.game.Game;
import forge.game.GlobalRuleChange;
import forge.game.ability.AbilityUtils;
import forge.game.card.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.*;
import forge.game.card.CardPredicates.Presets;
import forge.game.phase.PhaseHandler;
import forge.game.phase.PhaseType;
@@ -172,6 +167,18 @@ public class SetStateAi extends SpellAbilityAi {
private boolean shouldTurnFace(Card card, Player ai, PhaseHandler ph) {
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
if (!card.getRules().getType().isPermanent()) {
return false;

View File

@@ -111,7 +111,7 @@ public abstract class TapAiBase extends SpellAbilityAi {
* @return a boolean.
*/
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();
CardCollection tapList = CardLists.filterControlledBy(game.getCardsIn(ZoneType.Battlefield), ai.getOpponents());
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.collect.Iterables;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCombat;
import forge.ai.SpellAbilityAi;
import forge.game.Game;
@@ -30,7 +31,7 @@ public class TapAllAi extends SpellAbilityAi {
// or during upkeep/begin combat?
final Card source = sa.getHostCard();
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
final Game game = ai.getGame();
if (game.getPhaseHandler().getPhase().isAfter(PhaseType.COMBAT_BEGIN)) {
@@ -126,8 +127,8 @@ public class TapAllAi extends SpellAbilityAi {
if (tgt != null) {
sa.resetTargets();
sa.getTargets().add(ai.getOpponent());
validTappables = ai.getOpponent().getCardsIn(ZoneType.Battlefield);
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
validTappables = ComputerUtil.getOpponentFor(ai).getCardsIn(ZoneType.Battlefield);
}
if (mandatory) {

View File

@@ -179,7 +179,7 @@ public class TokenAi extends SpellAbilityAi {
*/
final Card source = sa.getHostCard();
final Game game = ai.getGame();
final Player opp = ai.getOpponent();
final Player opp = ComputerUtil.getOpponentFor(ai);
if (ComputerUtil.preventRunAwayActivations(sa)) {
return false; // prevent infinite tokens?
@@ -254,13 +254,13 @@ public class TokenAi extends SpellAbilityAi {
num = (num == null) ? "1" : num;
final int nToSac = AbilityUtils.calculateAmount(topStack.getHostCard(), num, topStack);
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));
// only care about saving single creature for now
if (!list.isEmpty() && nTokens > 0 && list.size() == nToSac) {
ComputerUtilCard.sortByEvaluateCreature(list);
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));
if (ComputerUtilCard.evaluateCreature(token) < ComputerUtilCard.evaluateCreature(list.get(0))
&& list.contains(token)) {
@@ -278,7 +278,7 @@ public class TokenAi extends SpellAbilityAi {
if (tgt != null) {
sa.resetTargets();
if (tgt.canOnlyTgtOpponent()) {
sa.getTargets().add(ai.getOpponent());
sa.getTargets().add(ComputerUtil.getOpponentFor(ai));
} else {
sa.getTargets().add(ai);
}

View File

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

View File

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

View File

@@ -1,7 +1,5 @@
package forge.ai.ability;
import java.util.List;
import forge.ai.ComputerUtil;
import forge.ai.ComputerUtilCard;
import forge.ai.ComputerUtilCost;
@@ -13,6 +11,7 @@ import forge.game.card.CardCollection;
import forge.game.card.CardLists;
import forge.game.card.CardPredicates.Presets;
import forge.game.cost.Cost;
import forge.game.cost.CostTap;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerCollection;
@@ -20,6 +19,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import java.util.List;
public class UntapAi extends SpellAbilityAi {
@Override
protected boolean checkAiLogic(final Player ai, final SpellAbility sa, final String aiLogic) {
@@ -132,7 +133,7 @@ public class UntapAi extends SpellAbilityAi {
Player targetController = ai;
if (sa.isCurse()) {
targetController = ai.getOpponent();
targetController = ComputerUtil.getOpponentFor(ai);
}
CardCollection list = CardLists.getTargetableCards(targetController.getCardsIn(ZoneType.Battlefield), sa);
@@ -148,6 +149,24 @@ public class UntapAi extends SpellAbilityAi {
final String[] tappablePermanents = {"Creature", "Land", "Artifact"};
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();
while (sa.getTargets().getNumTargeted() < tgt.getMaxTargets(sa.getHostCard(), sa)) {
Card choice = null;

View File

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

View File

@@ -204,7 +204,7 @@ public class GameSimulator {
}
// TODO: Support multiple opponents.
Player opponent = aiPlayer.getOpponent();
Player opponent = ComputerUtil.getOpponentFor(aiPlayer);
resolveStack(simGame, opponent);
// 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.card.Card;
import forge.game.card.CounterType;
import forge.game.combat.Combat;
import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.zone.ZoneType;

View File

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

View File

@@ -411,11 +411,13 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
}
@Override
public CardTypeView getTypeWithChanges(final Map<Long, CardChangedType> changedCardTypes) {
if (changedCardTypes.isEmpty()) { return this; }
public CardTypeView getTypeWithChanges(final Iterable<CardChangedType> changedCardTypes) {
CardType newType = null;
// we assume that changes are already correctly ordered (taken from TreeMap.values())
for (final CardChangedType ct : changedCardTypes) {
if(null == newType)
newType = new CardType(CardType.this);
final CardType newType = new CardType(CardType.this);
for (final CardChangedType ct : changedCardTypes.values()) {
if (ct.isRemoveCardTypes()) {
newType.coreTypes.clear();
}
@@ -441,7 +443,7 @@ public final class CardType implements Comparable<CardType>, CardTypeView {
newType.addAll(ct.getAddType());
}
}
return newType;
return newType == null ? this : newType;
}
@Override

View File

@@ -1,7 +1,6 @@
package forge.card;
import java.io.Serializable;
import java.util.Map;
import java.util.Set;
import forge.card.CardType.CoreType;
@@ -39,5 +38,5 @@ public interface CardTypeView extends Iterable<String>, Serializable {
boolean isPhenomenon();
boolean isEmblem();
boolean isTribal();
CardTypeView getTypeWithChanges(Map<Long, CardChangedType> changedCardTypes);
CardTypeView getTypeWithChanges(Iterable<CardChangedType> changedCardTypes);
}

View File

@@ -78,10 +78,6 @@ public class DeckGenerator5Color extends DeckGeneratorBase {
colors = ColorSet.fromMask(0).inverse();
}
private void initialize(DeckFormat format0) {
format0.adjustCMCLevels(cmcLevels);
colors = ColorSet.fromMask(0).inverse();
}
@Override
public final CardPool getDeck(final int size, final boolean forAi) {

View File

@@ -36,7 +36,6 @@ import forge.util.MyRandom;
import org.apache.commons.lang3.tuple.ImmutablePair;
import java.awt.print.Paper;
import java.util.*;
import java.util.Map.Entry;
import java.util.regex.Matcher;

View File

@@ -23,8 +23,8 @@ import com.google.common.collect.ImmutableList;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.BoosterSlots;
import forge.card.CardEdition;
import forge.item.generation.BoosterSlots;
import forge.util.MyRandom;
import org.apache.commons.lang3.tuple.Pair;

View File

@@ -28,8 +28,8 @@ import com.google.common.base.Function;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.BoosterGenerator;
import forge.card.CardEdition;
import forge.item.generation.BoosterGenerator;
import forge.util.TextUtil;
import forge.util.storage.StorageReaderFile;

View File

@@ -24,9 +24,9 @@ import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import forge.StaticData;
import forge.card.BoosterGenerator;
import forge.card.BoosterSlots;
import forge.card.CardRulesPredicates;
import forge.item.generation.BoosterGenerator;
import forge.item.generation.BoosterSlots;
import forge.util.Aggregates;
import forge.util.TextUtil;
import forge.util.storage.StorageReaderFile;

View File

@@ -21,8 +21,8 @@ import com.google.common.base.Function;
import forge.ImageKeys;
import forge.StaticData;
import forge.card.BoosterGenerator;
import forge.card.CardEdition;
import forge.item.generation.BoosterGenerator;
import java.util.List;

View File

@@ -1,487 +1,500 @@
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.card;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.StaticData;
import forge.card.CardEdition.FoilType;
import forge.item.IPaperCard;
import forge.item.IPaperCard.Predicates.Presets;
import forge.item.PaperCard;
import forge.item.SealedProduct;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
/**
* <p>
* BoosterGenerator class.
* </p>
*
* @author Forge
* @version $Id$
*/
public class BoosterGenerator {
private final static Map<String, PrintSheet> cachedSheets = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private static synchronized PrintSheet getPrintSheet(String key) {
if( !cachedSheets.containsKey(key) )
cachedSheets.put(key, makeSheet(key, StaticData.instance().getCommonCards().getAllCards()));
return cachedSheets.get(key);
}
private static PaperCard generateFoilCard(PrintSheet sheet) {
return StaticData.instance().getCommonCards().getFoiled(sheet.random(1, true).get(0));
}
private static PaperCard generateFoilCard(List<PaperCard> cardList) {
Collections.shuffle(cardList);
return StaticData.instance().getCommonCards().getFoiled(cardList.get(0));
}
public static List<PaperCard> getBoosterPack(SealedProduct.Template template) {
// TODO: tweak the chances of generating Masterpieces to be more authentic
// (currently merely added to the Rare/Mythic Rare print sheet via ExtraFoilSheetKey)
List<PaperCard> result = new ArrayList<>();
List<PrintSheet> sheetsUsed = new ArrayList<>();
CardEdition edition = StaticData.instance().getEditions().get(template.getEdition());
boolean hasFoil = edition != null && !template.getSlots().isEmpty() && MyRandom.getRandom().nextDouble() < edition.getFoilChanceInBooster() && edition.getFoilType() != FoilType.NOT_SUPPORTED;
boolean foilAtEndOfPack = hasFoil && edition.getFoilAlwaysInCommonSlot();
Random rand = new Random();
// Foil chances
// 1 Rare or Mythic rare (distribution ratio same as nonfoils)
// 2-3 Uncommons
// 4-6 Commons or Basic Lands
// 7 Time Shifted
// 8 VMA Special
// 9 DFC
// if result not valid for pack, reroll
// Other special types of foil slots, add here
CardRarity foilCard = CardRarity.Unknown;
while (foilCard == CardRarity.Unknown) {
int randomNum = rand.nextInt(9) + 1;
switch (randomNum) {
case 1:
// Rare or Mythic
foilCard = CardRarity.Rare;
break;
case 2:
case 3:
// Uncommon
foilCard = CardRarity.Uncommon;
break;
case 4:
case 5:
case 6:
// Common or Basic Land
foilCard = CardRarity.Common;
break;
case 7:
// Time Spiral
if (edition != null) {
if (edition.getName().equals("Time Spiral")) {
foilCard = CardRarity.Special;
}
}
break;
case 8:
if (edition != null) {
if (edition.getName().equals("Vintage Masters")) {
// 1 in 53 packs, with 7 possibilities for the slot itself in VMA
// (1 RareMythic, 2 Uncommon, 3 Common, 1 Special)
if (rand.nextInt(53) <= 7) {
foilCard = CardRarity.Special;
}
}
}
break;
case 9:
if (edition != null) {
// every sixth foil - same as rares
if (template.hasSlot("dfc")) {
foilCard = CardRarity.Special;
}
}
break;
// Insert any additional special rarities/slot types for special
// sets here, for 11 and up
default:
foilCard = CardRarity.Common;
// otherwise, default is Common or Basic Land
break;
}
}
String extraFoilSheetKey = edition != null ? edition.getAdditionalSheetForFoils() : "";
boolean replaceCommon = edition != null && !template.getSlots().isEmpty()
&& MyRandom.getRandom().nextDouble() < edition.getChanceReplaceCommonWith();
// Default, if no matching slot type is found : equal chance for each slot
// should not have effect unless new sets that do not match existing
// rarities are added
String foilSlot = Aggregates.random(template.getSlots()).getLeft().split("[ :!]")[0];
if (hasFoil) {
switch (foilCard) {
case Rare:
// Take whichever rare slot the pack has.
// Not the "MYTHIC" slot, no pack has that AFAIK
// and chance was for rare/mythic.
if (template.hasSlot(BoosterSlots.RARE_MYTHIC)) {
foilSlot = BoosterSlots.RARE_MYTHIC;
} else if (template.hasSlot(BoosterSlots.RARE)) {
foilSlot = BoosterSlots.RARE;
} else if (template.hasSlot(BoosterSlots.UNCOMMON_RARE)) {
foilSlot = BoosterSlots.UNCOMMON_RARE;
}
break;
case Uncommon:
if (template.hasSlot(BoosterSlots.UNCOMMON)) {
foilSlot = BoosterSlots.UNCOMMON;
} else if (template.hasSlot(BoosterSlots.UNCOMMON_RARE)) {
foilSlot = BoosterSlots.UNCOMMON_RARE;
}
break;
case Common:
foilSlot = BoosterSlots.COMMON;
if (template.hasSlot(BoosterSlots.BASIC_LAND)) {
// According to information I found, Basic Lands
// are on the common foil sheet, each type appearing once.
// Large Sets usually have 110 commons and 20 lands.
if (rand.nextInt(130) <= 20) {
foilSlot = BoosterSlots.BASIC_LAND;
}
}
break;
case Special:
if (template.hasSlot(BoosterSlots.TIME_SHIFTED)) {
foilSlot = BoosterSlots.TIME_SHIFTED;
} else if (template.hasSlot(BoosterSlots.DUAL_FACED_CARD)) {
foilSlot = BoosterSlots.DUAL_FACED_CARD;
} else if (template.hasSlot(BoosterSlots.SPECIAL)) {
foilSlot = BoosterSlots.SPECIAL;
}
break;
}
}
List<PaperCard> foilCardGeneratedAndHeld = new ArrayList<>();
for (Pair<String, Integer> slot : template.getSlots()) {
String slotType = slot.getLeft(); // add expansion symbol here?
int numCards = slot.getRight();
String[] sType = TextUtil.splitWithParenthesis(slotType, ' ');
String setCode = sType.length == 1 && template.getEdition() != null ? template.getEdition() : null;
String sheetKey = StaticData.instance().getEditions().contains(setCode) ? slotType.trim() + " " + setCode
: slotType.trim();
slotType = slotType.split("[ :!]")[0]; // add expansion symbol here?
boolean foilInThisSlot = hasFoil && (slotType.equals(foilSlot));
if ((!foilAtEndOfPack && foilInThisSlot)
|| (foilAtEndOfPack && hasFoil && slotType.startsWith(BoosterSlots.COMMON))) {
numCards--;
}
if (replaceCommon && slotType.startsWith(BoosterSlots.COMMON)) {
numCards--;
String replaceKey = StaticData.instance().getEditions().contains(setCode)
? edition.getSlotReplaceCommonWith().trim() + " " + setCode
: edition.getSlotReplaceCommonWith().trim();
PrintSheet replaceSheet = getPrintSheet(replaceKey);
result.addAll(replaceSheet.random(1, true));
sheetsUsed.add(replaceSheet);
replaceCommon = false;
}
PrintSheet ps = getPrintSheet(sheetKey);
result.addAll(ps.random(numCards, true));
sheetsUsed.add(ps);
if (foilInThisSlot) {
if (!foilAtEndOfPack) {
hasFoil = false;
if (!extraFoilSheetKey.isEmpty()) {
// TODO: extra foil sheets are currently reliably supported
// only for boosters with FoilAlwaysInCommonSlot=True.
// If FoilAlwaysInCommonSlot is false, a card from the extra
// sheet may still replace a card in any slot.
List<PaperCard> foilCards = new ArrayList<>();
for (PaperCard card : ps.toFlatList()) {
if (!foilCards.contains(card)) {
foilCards.add(card);
}
}
addCardsFromExtraSheet(foilCards, extraFoilSheetKey);
result.add(generateFoilCard(foilCards));
} else {
result.add(generateFoilCard(ps));
}
} else { // foilAtEndOfPack
if (!extraFoilSheetKey.isEmpty()) {
// TODO: extra foil sheets are currently reliably supported
// only for boosters with FoilAlwaysInCommonSlot=True.
// If FoilAlwaysInCommonSlot is false, a card from the extra
// sheet may still replace a card in any slot.
List<PaperCard> foilCards = new ArrayList<>();
for (PaperCard card : ps.toFlatList()) {
if (!foilCards.contains(card)) {
foilCards.add(card);
}
}
addCardsFromExtraSheet(foilCards, extraFoilSheetKey);
foilCardGeneratedAndHeld.add(generateFoilCard(foilCards));
} else {
if (edition != null) {
if (edition.getName().equals("Vintage Masters")) {
// Vintage Masters foil slot
// If "Special" was picked here, either foil or
// nonfoil P9 needs to be generated
// 1 out of ~30 normal and mythic rares are foil,
// match that.
// If not special card, make it always foil.
if ((rand.nextInt(30) == 1) || (foilSlot != BoosterSlots.SPECIAL)) {
foilCardGeneratedAndHeld.add(generateFoilCard(ps));
} else {
// Otherwise it's not foil (even though this is the
// foil slot!)
result.addAll(ps.random(1, true));
}
} else {
foilCardGeneratedAndHeld.add(generateFoilCard(ps));
}
}
}
}
}
}
if (hasFoil && foilAtEndOfPack) {
result.addAll(foilCardGeneratedAndHeld);
}
return result;
}
public static void addCardsFromExtraSheet(List<PaperCard> dest, String printSheetKey) {
PrintSheet extraSheet = getPrintSheet(printSheetKey);
// try to determine the allowed rarity of the cards in dest
Set<CardRarity> allowedRarity = Sets.newHashSet();
if (!dest.isEmpty()) {
for (PaperCard inDest : dest) {
allowedRarity.add(inDest.getRarity());
}
}
for (PaperCard card : extraSheet.toFlatList()) {
if (!dest.contains(card) && allowedRarity.contains(card.getRarity())) {
dest.add(card);
}
}
}
@SuppressWarnings("unchecked")
public static PrintSheet makeSheet(String sheetKey, Iterable<PaperCard> src) {
PrintSheet ps = new PrintSheet(sheetKey);
String[] sKey = TextUtil.splitWithParenthesis(sheetKey, ' ', 2);
Predicate<PaperCard> setPred = (Predicate<PaperCard>) (sKey.length > 1 ? IPaperCard.Predicates.printedInSets(sKey[1].split(" ")) : Predicates.alwaysTrue());
List<String> operators = new LinkedList<>(Arrays.asList(TextUtil.splitWithParenthesis(sKey[0], ':')));
Predicate<PaperCard> extraPred = buildExtraPredicate(operators);
// source replacement operators - if one is applied setPredicate will be ignored
Iterator<String> itMod = operators.iterator();
while(itMod.hasNext()) {
String mainCode = itMod.next();
if (mainCode.regionMatches(true, 0, "fromSheet", 0, 9)) { // custom print sheet
String sheetName = StringUtils.strip(mainCode.substring(9), "()\" ");
src = StaticData.instance().getPrintSheets().get(sheetName).toFlatList();
setPred = Predicates.alwaysTrue();
} else if (mainCode.startsWith("promo")) { // get exactly the named cards, that's a tiny inlined print sheet
String list = StringUtils.strip(mainCode.substring(5), "() ");
String[] cardNames = TextUtil.splitWithParenthesis(list, ',', '"', '"');
List<PaperCard> srcList = new ArrayList<>();
for(String cardName: cardNames) {
srcList.add(StaticData.instance().getCommonCards().getCard(cardName));
}
src = srcList;
setPred = Predicates.alwaysTrue();
} else {
continue;
}
itMod.remove();
}
// only special operators should remain by now - the ones that could not be turned into one predicate
String mainCode = operators.isEmpty() ? null : operators.get(0).trim();
if( null == mainCode || mainCode.equalsIgnoreCase(BoosterSlots.ANY) ) { // no restriction on rarity
Predicate<PaperCard> predicate = Predicates.and(setPred, extraPred);
ps.addAll(Iterables.filter(src, predicate));
} else if ( mainCode.equalsIgnoreCase(BoosterSlots.UNCOMMON_RARE) ) { // for sets like ARN, where U1 cards are considered rare and U3 are uncommon
Predicate<PaperCard> predicateRares = Predicates.and(setPred, IPaperCard.Predicates.Presets.IS_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateRares));
Predicate<PaperCard> predicateUncommon = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_UNCOMMON, extraPred);
ps.addAll(Iterables.filter(src, predicateUncommon), 3);
} else if ( mainCode.equalsIgnoreCase(BoosterSlots.RARE_MYTHIC) ) {
// Typical ratio of rares to mythics is 53:15, changing to 35:10 in smaller sets.
// To achieve the desired 1:8 are all mythics are added once, and all rares added twice per print sheet.
Predicate<PaperCard> predicateMythic = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_MYTHIC_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateMythic));
Predicate<PaperCard> predicateRare = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateRare), 2);
} else {
throw new IllegalArgumentException("Booster generator: operator could not be parsed - " + mainCode);
}
return ps;
}
/**
* This method also modifies passed parameter
*/
private static Predicate<PaperCard> buildExtraPredicate(List<String> operators) {
List<Predicate<PaperCard>> conditions = new ArrayList<>();
Iterator<String> itOp = operators.iterator();
while(itOp.hasNext()) {
String operator = itOp.next();
if(StringUtils.isEmpty(operator)) {
itOp.remove();
continue;
}
if(operator.endsWith("s")) {
operator = operator.substring(0, operator.length() - 1);
}
boolean invert = operator.charAt(0) == '!';
if (invert) { operator = operator.substring(1); }
Predicate<PaperCard> toAdd = null;
if (operator.equalsIgnoreCase(BoosterSlots.DUAL_FACED_CARD)) {
toAdd = Predicates.compose(Predicates.or(CardRulesPredicates.splitType(CardSplitType.Transform), CardRulesPredicates.splitType(CardSplitType.Meld)),
PaperCard.FN_GET_RULES);
} else if (operator.equalsIgnoreCase(BoosterSlots.LAND)) { toAdd = Predicates.compose(CardRulesPredicates.Presets.IS_LAND, PaperCard.FN_GET_RULES);
} else if (operator.equalsIgnoreCase(BoosterSlots.BASIC_LAND)) { toAdd = IPaperCard.Predicates.Presets.IS_BASIC_LAND;
} else if (operator.equalsIgnoreCase(BoosterSlots.TIME_SHIFTED)) { toAdd = IPaperCard.Predicates.Presets.IS_SPECIAL;
} else if (operator.equalsIgnoreCase(BoosterSlots.SPECIAL)) { toAdd = IPaperCard.Predicates.Presets.IS_SPECIAL;
} else if (operator.equalsIgnoreCase(BoosterSlots.MYTHIC)) { toAdd = IPaperCard.Predicates.Presets.IS_MYTHIC_RARE;
} else if (operator.equalsIgnoreCase(BoosterSlots.RARE)) { toAdd = IPaperCard.Predicates.Presets.IS_RARE;
} else if (operator.equalsIgnoreCase(BoosterSlots.UNCOMMON)) { toAdd = IPaperCard.Predicates.Presets.IS_UNCOMMON;
} else if (operator.equalsIgnoreCase(BoosterSlots.COMMON)) { toAdd = IPaperCard.Predicates.Presets.IS_COMMON;
} else if (operator.startsWith("name(")) {
operator = StringUtils.strip(operator.substring(4), "() ");
String[] cardNames = TextUtil.splitWithParenthesis(operator, ',', '"', '"');
toAdd = IPaperCard.Predicates.names(Lists.newArrayList(cardNames));
} else if (operator.startsWith("color(")) {
operator = StringUtils.strip(operator.substring("color(".length() + 1), "()\" ");
switch (operator.toLowerCase()) {
case "black":
toAdd = Presets.IS_BLACK;
break;
case "blue":
toAdd = Presets.IS_BLUE;
break;
case "green":
toAdd = Presets.IS_GREEN;
break;
case "red":
toAdd = Presets.IS_RED;
break;
case "white":
toAdd = Presets.IS_WHITE;
break;
case "colorless":
toAdd = Presets.IS_COLORLESS;
break;
}
} else if (operator.startsWith("fromSets(")) {
operator = StringUtils.strip(operator.substring("fromSets(".length() + 1), "()\" ");
String[] sets = operator.split(",");
toAdd = IPaperCard.Predicates.printedInSets(sets);
} else if (operator.startsWith("fromSheet(") && invert) {
String sheetName = StringUtils.strip(operator.substring(9), "()\" ");
Iterable<PaperCard> src = StaticData.instance().getPrintSheets().get(sheetName).toFlatList();
List<String> cardNames = Lists.newArrayList();
for (PaperCard card : src) {
cardNames.add(card.getName());
}
toAdd = IPaperCard.Predicates.names(Lists.newArrayList(cardNames));
}
if (toAdd == null) {
continue;
} else {
itOp.remove();
}
if (invert) {
toAdd = Predicates.not(toAdd);
}
conditions.add(toAdd);
}
if (conditions.isEmpty()) {
return Predicates.alwaysTrue();
}
return Predicates.and(conditions);
}
}
/*
* Forge: Play Magic: the Gathering.
* Copyright (C) 2011 Forge Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package forge.item.generation;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import forge.StaticData;
import forge.card.CardEdition;
import forge.card.CardRarity;
import forge.card.CardRulesPredicates;
import forge.card.CardSplitType;
import forge.card.PrintSheet;
import forge.card.CardEdition.FoilType;
import forge.item.IPaperCard;
import forge.item.IPaperCard.Predicates.Presets;
import forge.item.PaperCard;
import forge.item.SealedProduct;
import forge.util.Aggregates;
import forge.util.MyRandom;
import forge.util.TextUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.tuple.Pair;
import java.util.*;
/**
* <p>
* BoosterGenerator class.
* </p>
*
* @author Forge
* @version $Id: BoosterGenerator.java 35014 2017-08-13 00:40:48Z Max mtg $
*/
public class BoosterGenerator {
private final static Map<String, PrintSheet> cachedSheets = new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private static synchronized PrintSheet getPrintSheet(String key) {
if( !cachedSheets.containsKey(key) )
cachedSheets.put(key, makeSheet(key, StaticData.instance().getCommonCards().getAllCards()));
return cachedSheets.get(key);
}
private static PaperCard generateFoilCard(PrintSheet sheet) {
return StaticData.instance().getCommonCards().getFoiled(sheet.random(1, true).get(0));
}
private static PaperCard generateFoilCard(List<PaperCard> cardList) {
Collections.shuffle(cardList);
return StaticData.instance().getCommonCards().getFoiled(cardList.get(0));
}
public static List<PaperCard> getBoosterPack(SealedProduct.Template template) {
// TODO: tweak the chances of generating Masterpieces to be more authentic
// (currently merely added to the Rare/Mythic Rare print sheet via ExtraFoilSheetKey)
List<PaperCard> result = new ArrayList<>();
List<PrintSheet> sheetsUsed = new ArrayList<>();
CardEdition edition = StaticData.instance().getEditions().get(template.getEdition());
boolean hasFoil = edition != null
&& !template.getSlots().isEmpty()
&& MyRandom.getRandom().nextDouble() < edition.getFoilChanceInBooster()
&& edition.getFoilType() != FoilType.NOT_SUPPORTED;
boolean foilAtEndOfPack = hasFoil && edition.getFoilAlwaysInCommonSlot();
Random rand = new Random();
// Foil chances
// 1 Rare or Mythic rare (distribution ratio same as nonfoils)
// 2-3 Uncommons
// 4-6 Commons or Basic Lands
// 7 Time Shifted
// 8 VMA Special
// 9 DFC
// if result not valid for pack, reroll
// Other special types of foil slots, add here
CardRarity foilCard = CardRarity.Unknown;
while (foilCard == CardRarity.Unknown) {
int randomNum = rand.nextInt(9) + 1;
switch (randomNum) {
case 1:
// Rare or Mythic
foilCard = CardRarity.Rare;
break;
case 2:
case 3:
// Uncommon
foilCard = CardRarity.Uncommon;
break;
case 4:
case 5:
case 6:
// Common or Basic Land
foilCard = CardRarity.Common;
break;
case 7:
// Time Spiral
if (edition != null) {
if (edition.getName().equals("Time Spiral")) {
foilCard = CardRarity.Special;
}
}
break;
case 8:
if (edition != null) {
if (edition.getName().equals("Vintage Masters")) {
// 1 in 53 packs, with 7 possibilities for the slot itself in VMA
// (1 RareMythic, 2 Uncommon, 3 Common, 1 Special)
if (rand.nextInt(53) <= 7) {
foilCard = CardRarity.Special;
}
}
}
break;
case 9:
if (edition != null) {
// every sixth foil - same as rares
if (template.hasSlot("dfc")) {
foilCard = CardRarity.Special;
}
}
break;
// Insert any additional special rarities/slot types for special
// sets here, for 11 and up
default:
foilCard = CardRarity.Common;
// otherwise, default is Common or Basic Land
break;
}
}
String extraFoilSheetKey = edition != null ? edition.getAdditionalSheetForFoils() : "";
boolean replaceCommon = edition != null && !template.getSlots().isEmpty()
&& MyRandom.getRandom().nextDouble() < edition.getChanceReplaceCommonWith();
String foilSlot = "";
if (hasFoil) {
// Default, if no matching slot type is found : equal chance for each slot
// should not have effect unless new sets that do not match existing
// rarities are added
foilSlot = Aggregates.random(template.getSlots()).getLeft().split("[ :!]")[0];
switch (foilCard) {
case Rare:
// Take whichever rare slot the pack has.
// Not the "MYTHIC" slot, no pack has that AFAIK
// and chance was for rare/mythic.
if (template.hasSlot(BoosterSlots.RARE_MYTHIC)) {
foilSlot = BoosterSlots.RARE_MYTHIC;
} else if (template.hasSlot(BoosterSlots.RARE)) {
foilSlot = BoosterSlots.RARE;
} else if (template.hasSlot(BoosterSlots.UNCOMMON_RARE)) {
foilSlot = BoosterSlots.UNCOMMON_RARE;
}
break;
case Uncommon:
if (template.hasSlot(BoosterSlots.UNCOMMON)) {
foilSlot = BoosterSlots.UNCOMMON;
} else if (template.hasSlot(BoosterSlots.UNCOMMON_RARE)) {
foilSlot = BoosterSlots.UNCOMMON_RARE;
}
break;
case Common:
foilSlot = BoosterSlots.COMMON;
if (template.hasSlot(BoosterSlots.BASIC_LAND)) {
// According to information I found, Basic Lands
// are on the common foil sheet, each type appearing once.
// Large Sets usually have 110 commons and 20 lands.
if (rand.nextInt(130) <= 20) {
foilSlot = BoosterSlots.BASIC_LAND;
}
}
break;
case Special:
if (template.hasSlot(BoosterSlots.TIME_SHIFTED)) {
foilSlot = BoosterSlots.TIME_SHIFTED;
} else if (template.hasSlot(BoosterSlots.DUAL_FACED_CARD)) {
foilSlot = BoosterSlots.DUAL_FACED_CARD;
} else if (template.hasSlot(BoosterSlots.SPECIAL)) {
foilSlot = BoosterSlots.SPECIAL;
}
break;
default:
break;
}
}
List<PaperCard> foilCardGeneratedAndHeld = new ArrayList<>();
for (Pair<String, Integer> slot : template.getSlots()) {
String slotType = slot.getLeft(); // add expansion symbol here?
int numCards = slot.getRight();
String[] sType = TextUtil.splitWithParenthesis(slotType, ' ');
String setCode = sType.length == 1 && template.getEdition() != null ? template.getEdition() : null;
String sheetKey = StaticData.instance().getEditions().contains(setCode) ? slotType.trim() + " " + setCode
: slotType.trim();
slotType = slotType.split("[ :!]")[0]; // add expansion symbol here?
boolean foilInThisSlot = hasFoil && (slotType.equals(foilSlot));
if ((!foilAtEndOfPack && foilInThisSlot)
|| (foilAtEndOfPack && hasFoil && slotType.startsWith(BoosterSlots.COMMON))) {
numCards--;
}
if (replaceCommon && slotType.startsWith(BoosterSlots.COMMON)) {
numCards--;
String replaceKey = StaticData.instance().getEditions().contains(setCode)
? edition.getSlotReplaceCommonWith().trim() + " " + setCode
: edition.getSlotReplaceCommonWith().trim();
PrintSheet replaceSheet = getPrintSheet(replaceKey);
result.addAll(replaceSheet.random(1, true));
sheetsUsed.add(replaceSheet);
replaceCommon = false;
}
PrintSheet ps = getPrintSheet(sheetKey);
result.addAll(ps.random(numCards, true));
sheetsUsed.add(ps);
if (foilInThisSlot) {
if (!foilAtEndOfPack) {
hasFoil = false;
if (!extraFoilSheetKey.isEmpty()) {
// TODO: extra foil sheets are currently reliably supported
// only for boosters with FoilAlwaysInCommonSlot=True.
// If FoilAlwaysInCommonSlot is false, a card from the extra
// sheet may still replace a card in any slot.
List<PaperCard> foilCards = new ArrayList<>();
for (PaperCard card : ps.toFlatList()) {
if (!foilCards.contains(card)) {
foilCards.add(card);
}
}
addCardsFromExtraSheet(foilCards, extraFoilSheetKey);
result.add(generateFoilCard(foilCards));
} else {
result.add(generateFoilCard(ps));
}
} else { // foilAtEndOfPack
if (!extraFoilSheetKey.isEmpty()) {
// TODO: extra foil sheets are currently reliably supported
// only for boosters with FoilAlwaysInCommonSlot=True.
// If FoilAlwaysInCommonSlot is false, a card from the extra
// sheet may still replace a card in any slot.
List<PaperCard> foilCards = new ArrayList<>();
for (PaperCard card : ps.toFlatList()) {
if (!foilCards.contains(card)) {
foilCards.add(card);
}
}
addCardsFromExtraSheet(foilCards, extraFoilSheetKey);
foilCardGeneratedAndHeld.add(generateFoilCard(foilCards));
} else {
if (edition != null) {
if (edition.getName().equals("Vintage Masters")) {
// Vintage Masters foil slot
// If "Special" was picked here, either foil or
// nonfoil P9 needs to be generated
// 1 out of ~30 normal and mythic rares are foil,
// match that.
// If not special card, make it always foil.
if ((rand.nextInt(30) == 1) || (foilSlot != BoosterSlots.SPECIAL)) {
foilCardGeneratedAndHeld.add(generateFoilCard(ps));
} else {
// Otherwise it's not foil (even though this is the
// foil slot!)
result.addAll(ps.random(1, true));
}
} else {
foilCardGeneratedAndHeld.add(generateFoilCard(ps));
}
}
}
}
}
}
if (hasFoil && foilAtEndOfPack) {
result.addAll(foilCardGeneratedAndHeld);
}
return result;
}
public static void addCardsFromExtraSheet(List<PaperCard> dest, String printSheetKey) {
PrintSheet extraSheet = getPrintSheet(printSheetKey);
// try to determine the allowed rarity of the cards in dest
Set<CardRarity> allowedRarity = Sets.newHashSet();
if (!dest.isEmpty()) {
for (PaperCard inDest : dest) {
allowedRarity.add(inDest.getRarity());
}
}
for (PaperCard card : extraSheet.toFlatList()) {
if (!dest.contains(card) && allowedRarity.contains(card.getRarity())) {
dest.add(card);
}
}
}
@SuppressWarnings("unchecked")
public static PrintSheet makeSheet(String sheetKey, Iterable<PaperCard> src) {
PrintSheet ps = new PrintSheet(sheetKey);
String[] sKey = TextUtil.splitWithParenthesis(sheetKey, ' ', 2);
Predicate<PaperCard> setPred = (Predicate<PaperCard>) (sKey.length > 1 ? IPaperCard.Predicates.printedInSets(sKey[1].split(" ")) : Predicates.alwaysTrue());
List<String> operators = new LinkedList<>(Arrays.asList(TextUtil.splitWithParenthesis(sKey[0], ':')));
Predicate<PaperCard> extraPred = buildExtraPredicate(operators);
// source replacement operators - if one is applied setPredicate will be ignored
Iterator<String> itMod = operators.iterator();
while(itMod.hasNext()) {
String mainCode = itMod.next();
if (mainCode.regionMatches(true, 0, "fromSheet", 0, 9)) { // custom print sheet
String sheetName = StringUtils.strip(mainCode.substring(9), "()\" ");
src = StaticData.instance().getPrintSheets().get(sheetName).toFlatList();
setPred = Predicates.alwaysTrue();
} else if (mainCode.startsWith("promo") || mainCode.startsWith("name")) { // get exactly the named cards, that's a tiny inlined print sheet
String list = StringUtils.strip(mainCode.substring(5), "() ");
String[] cardNames = TextUtil.splitWithParenthesis(list, ',', '"', '"');
List<PaperCard> srcList = new ArrayList<>();
for(String cardName: cardNames) {
srcList.add(StaticData.instance().getCommonCards().getCard(cardName));
}
src = srcList;
setPred = Predicates.alwaysTrue();
} else {
continue;
}
itMod.remove();
}
// only special operators should remain by now - the ones that could not be turned into one predicate
String mainCode = operators.isEmpty() ? null : operators.get(0).trim();
if( null == mainCode || mainCode.equalsIgnoreCase(BoosterSlots.ANY) ) { // no restriction on rarity
Predicate<PaperCard> predicate = Predicates.and(setPred, extraPred);
ps.addAll(Iterables.filter(src, predicate));
} else if ( mainCode.equalsIgnoreCase(BoosterSlots.UNCOMMON_RARE) ) { // for sets like ARN, where U1 cards are considered rare and U3 are uncommon
Predicate<PaperCard> predicateRares = Predicates.and(setPred, IPaperCard.Predicates.Presets.IS_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateRares));
Predicate<PaperCard> predicateUncommon = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_UNCOMMON, extraPred);
ps.addAll(Iterables.filter(src, predicateUncommon), 3);
} else if ( mainCode.equalsIgnoreCase(BoosterSlots.RARE_MYTHIC) ) {
// Typical ratio of rares to mythics is 53:15, changing to 35:10 in smaller sets.
// To achieve the desired 1:8 are all mythics are added once, and all rares added twice per print sheet.
Predicate<PaperCard> predicateMythic = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_MYTHIC_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateMythic));
Predicate<PaperCard> predicateRare = Predicates.and( setPred, IPaperCard.Predicates.Presets.IS_RARE, extraPred);
ps.addAll(Iterables.filter(src, predicateRare), 2);
} else {
throw new IllegalArgumentException("Booster generator: operator could not be parsed - " + mainCode);
}
return ps;
}
/**
* This method also modifies passed parameter
*/
private static Predicate<PaperCard> buildExtraPredicate(List<String> operators) {
List<Predicate<PaperCard>> conditions = new ArrayList<>();
Iterator<String> itOp = operators.iterator();
while(itOp.hasNext()) {
String operator = itOp.next();
if(StringUtils.isEmpty(operator)) {
itOp.remove();
continue;
}
if(operator.endsWith("s")) {
operator = operator.substring(0, operator.length() - 1);
}
boolean invert = operator.charAt(0) == '!';
if (invert) { operator = operator.substring(1); }
Predicate<PaperCard> toAdd = null;
if (operator.equalsIgnoreCase(BoosterSlots.DUAL_FACED_CARD)) {
toAdd = Predicates.compose(Predicates.or(CardRulesPredicates.splitType(CardSplitType.Transform), CardRulesPredicates.splitType(CardSplitType.Meld)),
PaperCard.FN_GET_RULES);
} else if (operator.equalsIgnoreCase(BoosterSlots.LAND)) { toAdd = Predicates.compose(CardRulesPredicates.Presets.IS_LAND, PaperCard.FN_GET_RULES);
} else if (operator.equalsIgnoreCase(BoosterSlots.BASIC_LAND)) { toAdd = IPaperCard.Predicates.Presets.IS_BASIC_LAND;
} else if (operator.equalsIgnoreCase(BoosterSlots.TIME_SHIFTED)) { toAdd = IPaperCard.Predicates.Presets.IS_SPECIAL;
} else if (operator.equalsIgnoreCase(BoosterSlots.SPECIAL)) { toAdd = IPaperCard.Predicates.Presets.IS_SPECIAL;
} else if (operator.equalsIgnoreCase(BoosterSlots.MYTHIC)) { toAdd = IPaperCard.Predicates.Presets.IS_MYTHIC_RARE;
} else if (operator.equalsIgnoreCase(BoosterSlots.RARE)) { toAdd = IPaperCard.Predicates.Presets.IS_RARE;
} else if (operator.equalsIgnoreCase(BoosterSlots.UNCOMMON)) { toAdd = IPaperCard.Predicates.Presets.IS_UNCOMMON;
} else if (operator.equalsIgnoreCase(BoosterSlots.COMMON)) { toAdd = IPaperCard.Predicates.Presets.IS_COMMON;
} else if (operator.startsWith("name(")) {
operator = StringUtils.strip(operator.substring(4), "() ");
String[] cardNames = TextUtil.splitWithParenthesis(operator, ',', '"', '"');
toAdd = IPaperCard.Predicates.names(Lists.newArrayList(cardNames));
} else if (operator.startsWith("color(")) {
operator = StringUtils.strip(operator.substring("color(".length() + 1), "()\" ");
switch (operator.toLowerCase()) {
case "black":
toAdd = Presets.IS_BLACK;
break;
case "blue":
toAdd = Presets.IS_BLUE;
break;
case "green":
toAdd = Presets.IS_GREEN;
break;
case "red":
toAdd = Presets.IS_RED;
break;
case "white":
toAdd = Presets.IS_WHITE;
break;
case "colorless":
toAdd = Presets.IS_COLORLESS;
break;
}
} else if (operator.startsWith("fromSets(")) {
operator = StringUtils.strip(operator.substring("fromSets(".length() + 1), "()\" ");
String[] sets = operator.split(",");
toAdd = IPaperCard.Predicates.printedInSets(sets);
} else if (operator.startsWith("fromSheet(") && invert) {
String sheetName = StringUtils.strip(operator.substring(9), "()\" ");
Iterable<PaperCard> src = StaticData.instance().getPrintSheets().get(sheetName).toFlatList();
List<String> cardNames = Lists.newArrayList();
for (PaperCard card : src) {
cardNames.add(card.getName());
}
toAdd = IPaperCard.Predicates.names(Lists.newArrayList(cardNames));
}
if (toAdd == null) {
continue;
} else {
itOp.remove();
}
if (invert) {
toAdd = Predicates.not(toAdd);
}
conditions.add(toAdd);
}
if (conditions.isEmpty()) {
return Predicates.alwaysTrue();
}
return Predicates.and(conditions);
}
}

View File

@@ -1,4 +1,4 @@
package forge.card;
package forge.item.generation;
public class BoosterSlots {
public static final String LAND = "Land";

View File

@@ -1,4 +1,4 @@
package forge.card;
package forge.item.generation;
import com.google.common.base.Supplier;
import forge.item.PaperCard;

View File

@@ -1,8 +1,9 @@
package forge.card;
package forge.item.generation;
import com.google.common.base.Predicate;
import com.google.common.collect.Iterables;
import forge.StaticData;
import forge.card.PrintSheet;
import forge.item.PaperCard;
import forge.item.SealedProduct;
import forge.util.ItemPool;

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>forge</artifactId>
<groupId>forge</groupId>
<version>1.6.1</version>
<version>1.6.2</version>
</parent>
<artifactId>forge-game</artifactId>

View File

@@ -0,0 +1,182 @@
package forge.game;
import forge.card.ColorSet;
import forge.card.MagicColor;
import forge.game.ability.AbilityUtils;
import forge.game.card.Card;
import forge.game.card.CardState;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Expressions;
public class ForgeScript {
public static boolean cardStateHasProperty(CardState cardState, String property, Player sourceController,
Card source, SpellAbility spellAbility) {
final boolean isColorlessSource = cardState.getCard().hasKeyword("Colorless Damage Source", cardState);
final ColorSet colors = cardState.getCard().determineColor(cardState);
if (property.contains("White") || property.contains("Blue") || property.contains("Black")
|| property.contains("Red") || property.contains("Green")) {
boolean mustHave = !property.startsWith("non");
boolean withSource = property.endsWith("Source");
if (withSource && isColorlessSource) {
return false;
}
final String colorName = property.substring(mustHave ? 0 : 3, property.length() - (withSource ? 6 : 0));
int desiredColor = MagicColor.fromName(colorName);
boolean hasColor = colors.hasAnyColor(desiredColor);
if (mustHave != hasColor)
return false;
} else if (property.contains("Colorless")) { // ... Card is colorless
boolean non = property.startsWith("non");
boolean withSource = property.endsWith("Source");
if (non && withSource && isColorlessSource) {
return false;
}
if (non == colors.isColorless()) return false;
} else if (property.contains("MultiColor")) {
// ... Card is multicolored
if (property.endsWith("Source") && isColorlessSource)
return false;
if (property.startsWith("non") == colors.isMulticolor())
return false;
} else if (property.contains("MonoColor")) { // ... Card is monocolored
if (property.endsWith("Source") && isColorlessSource)
return false;
if (property.startsWith("non") == colors.isMonoColor())
return false;
} else if (property.startsWith("ChosenColor")) {
if (property.endsWith("Source") && isColorlessSource)
return false;
if (!source.hasChosenColor() || !colors.hasAnyColor(MagicColor.fromName(source.getChosenColor())))
return false;
} else if (property.startsWith("AnyChosenColor")) {
if (property.endsWith("Source") && isColorlessSource)
return false;
if (!source.hasChosenColor()
|| !colors.hasAnyColor(ColorSet.fromNames(source.getChosenColors()).getColor()))
return false;
} else if (property.startsWith("non")) {
// ... Other Card types
if (cardState.getTypeWithChanges().hasStringType(property.substring(3))) {
return false;
}
} else if (property.equals("CostsPhyrexianMana")) {
if (!cardState.getManaCost().hasPhyrexian()) {
return false;
}
} else if (property.startsWith("HasSVar")) {
final String svar = property.substring(8);
if (!cardState.hasSVar(svar)) {
return false;
}
} else if (property.equals("ChosenType")) {
if (!cardState.getTypeWithChanges().hasStringType(source.getChosenType())) {
return false;
}
} else if (property.equals("IsNotChosenType")) {
if (cardState.getTypeWithChanges().hasStringType(source.getChosenType())) {
return false;
}
} else if (property.startsWith("HasSubtype")) {
final String subType = property.substring(11);
if (!cardState.getTypeWithChanges().hasSubtype(subType)) {
return false;
}
} else if (property.startsWith("HasNoSubtype")) {
final String subType = property.substring(13);
if (cardState.getTypeWithChanges().hasSubtype(subType)) {
return false;
}
} else if (property.equals("hasActivatedAbilityWithTapCost")) {
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isAbility() && (sa.getPayCosts() != null) && sa.getPayCosts().hasTapCost()) {
return true;
}
}
return false;
} else if (property.equals("hasActivatedAbility")) {
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isAbility()) {
return true;
}
}
return false;
} else if (property.equals("hasManaAbility")) {
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isManaAbility()) {
return true;
}
}
return false;
} else if (property.equals("hasNonManaActivatedAbility")) {
for (final SpellAbility sa : cardState.getSpellAbilities()) {
if (sa.isAbility() && !sa.isManaAbility()) {
return true;
}
}
return false;
} else if (property.startsWith("cmc")) {
int x;
String rhs = property.substring(5);
int y = cardState.getManaCost().getCMC();
try {
x = Integer.parseInt(rhs);
} catch (final NumberFormatException e) {
x = AbilityUtils.calculateAmount(source, rhs, spellAbility);
}
if (!Expressions.compare(y, property, x)) {
return false;
}
} else if (!cardState.getTypeWithChanges().hasStringType(property)) {
return false;
}
return true;
}
public static boolean spellAbilityHasProperty(SpellAbility sa, String property, Player sourceController,
Card source, SpellAbility spellAbility) {
if (property.equals("Buyback")) {
if (!sa.isBuyBackAbility()) {
return false;
}
} else if (property.equals("Cycling")) {
if (!sa.isCycling()) {
return false;
}
} else if (property.equals("Dash")) {
if (!sa.isDash()) {
return false;
}
} else if (property.equals("Flashback")) {
if (!sa.isFlashBackAbility()) {
return false;
}
} else if (property.equals("MorphUp")) {
if (!sa.isMorphUp()) {
return false;
}
} else if (property.equals("Equip")) {
if (!sa.hasParam("Equip")) {
return false;
}
}
return true;
}
}

View File

@@ -23,6 +23,7 @@ import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.Iterables;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.eventbus.EventBus;
@@ -90,6 +91,9 @@ public class Game {
private CardCollection lastStateBattlefield = new CardCollection();
private CardCollection lastStateGraveyard = new CardCollection();
private Map<Player, PlayerCollection> attackedThisTurn = Maps.newHashMap();
private Map<Player, PlayerCollection> attackedLastTurn = Maps.newHashMap();
private Player monarch = null;
private Player monarchBeginTurn = null;
@@ -120,6 +124,28 @@ public class Game {
this.monarchBeginTurn = monarchBeginTurn;
}
public Map<Player, PlayerCollection> getPlayersAttackedThisTurn() {
return attackedThisTurn;
}
public Map<Player, PlayerCollection> getPlayersAttackedLastTurn() {
return attackedLastTurn;
}
public void addPlayerAttackedThisTurn(Player attacker, Player defender) {
PlayerCollection atk = attackedThisTurn.get(attacker);
if (atk == null) {
attackedThisTurn.put(attacker, new PlayerCollection());
}
attackedThisTurn.get(attacker).add(defender);
}
public void resetPlayersAttackedOnNextTurn() {
attackedLastTurn.clear();
attackedLastTurn.putAll(attackedThisTurn);
attackedThisTurn.clear();
}
public CardCollectionView getLastStateBattlefield() {
return lastStateBattlefield;
}
@@ -626,8 +652,13 @@ public class Game {
public void onPlayerLost(Player p) {
// Rule 800.4 Losing a Multiplayer game
CardCollectionView cards = this.getCardsInGame();
boolean planarControllerLost = false;
for(Card c : cards) {
if (c.getController().equals(p) && (c.isPlane() || c.isPhenomenon())) {
planarControllerLost = true;
}
if (c.getOwner().equals(p)) {
c.ceaseToExist();
} else {
@@ -636,6 +667,17 @@ public class Game {
this.getAction().exile(c, null);
}
}
}
// 901.6: If the current planar controller would leave the game, instead the next player
// in turn order that wouldnt leave the game becomes the planar controller, then the old
// planar controller leaves
// 901.10: When a player leaves the game, all objects owned by that player except abilities
// from phenomena leave the game. (See rule 800.4a.) If that includes a face-up plane card
// or phenomenon card, the planar controller turns the top card of his or her planar deck face up.the game.
if (planarControllerLost) {
getNextPlayerAfter(p).initPlane();
}
if (p != null && p.equals(getMonarch())) {

View File

@@ -19,6 +19,7 @@ package forge.game;
import com.google.common.base.Predicate;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ComparisonChain;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
@@ -257,6 +258,36 @@ public class GameAction {
}
}
// special rule for Worms of the Earth
if (toBattlefield && game.getStaticEffects().getGlobalRuleChange(GlobalRuleChange.noLandBattlefield)) {
// something that is already a Land cant enter the battlefield
if (c.isLand()) {
return c;
}
// check if something would be a land
Card noLandLKI = CardUtil.getLKICopy(c);
//setZone is not enough
//noLandLKI.setZone(zoneTo);
noLandLKI.setImmutable(true); // immu doesn't trigger Zone
zoneTo.add(noLandLKI);
noLandLKI.setImmutable(false); // but need to remove that or Static doesn't find it
checkStaticAbilities(false, Sets.newHashSet(noLandLKI));
boolean noLand = noLandLKI.isLand();
zoneTo.remove(noLandLKI);
// reset static
checkStaticAbilities(false, Sets.newHashSet(noLandLKI));
if(noLand) {
// if something would only be a land when entering the battlefield and not before
// put it into the graveyard instead
zoneTo = c.getOwner().getZone(ZoneType.Graveyard);
}
}
if (!suppress) {
if (zoneFrom == null) {
copied.getOwner().addInboundToken(copied);
@@ -367,7 +398,7 @@ public class GameAction {
runParams.put("Destination", zoneTo.getZoneType().name());
runParams.put("SpellAbilityStackInstance", game.stack.peek());
runParams.put("IndividualCostPaymentInstance", game.costPaymentStack.peek());
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, false);
game.getTriggerHandler().runTrigger(TriggerType.ChangesZone, runParams, true);
if (zoneFrom != null && zoneFrom.is(ZoneType.Battlefield)) {
final Map<String, Object> runParams2 = Maps.newHashMap();
runParams2.put("Card", lastKnownInfo);
@@ -436,6 +467,8 @@ public class GameAction {
copied.setState(CardStateName.Original, true);
}
unattachCardLeavingBattlefield(copied);
// Remove all changed keywords
copied.removeAllChangedText(game.getNextTimestamp());
} else if (toBattlefield) {
// reset timestamp in changezone effects so they have same timestamp if ETB simutaneously
copied.setTimestamp(game.getNextTimestamp());
@@ -715,11 +748,14 @@ public class GameAction {
}
});
// TODO Java 1.8 use comparingLong
final Comparator<StaticAbility> comp = new Comparator<StaticAbility>() {
@Override
public int compare(final StaticAbility a, final StaticAbility b) {
return Long.compare(a.getHostCard().getTimestamp(), b.getHostCard().getTimestamp());
return ComparisonChain.start()
.compareTrueFirst(a.hasParam("CharacteristicDefining"), b.hasParam("CharacteristicDefining"))
.compare(a.getHostCard().getTimestamp(), b.getHostCard().getTimestamp())
.result();
}
};
Collections.sort(staticAbilities, comp);

View File

@@ -285,6 +285,8 @@ public final class GameActionUtil {
case Retrace:
result.getRestrictions().setZone(ZoneType.Graveyard);
break;
default:
break;
}
}
return result;

View File

@@ -32,9 +32,11 @@ public enum GlobalRuleChange {
onlyOneAttackerATurn ("No more than one creature can attack each turn."),
onlyOneAttackerACombat ("No more than one creature can attack each combat."),
onlyOneBlocker ("No more than one creature can block each combat."),
onlyOneBlockerPerOpponent ("Each opponent can't block with more than one creature."),
onlyTwoBlockers ("No more than two creatures can block each combat."),
toughnessAssignsDamage ("Each creature assigns combat damage equal to its toughness rather than its power."),
blankIsChaos("Each blank roll of the planar dice is a {CHAOS} roll.");
blankIsChaos("Each blank roll of the planar dice is a {CHAOS} roll."),
noLandBattlefield("Lands can't enter the battlefield.");
private final String ruleText;

View File

@@ -1,22 +1,6 @@
package forge.game;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.HashMultiset;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multiset;
import com.google.common.collect.*;
import forge.LobbyPlayer;
import forge.deck.CardPool;
import forge.deck.Deck;
@@ -31,8 +15,11 @@ import forge.game.trigger.Trigger;
import forge.game.zone.PlayerZone;
import forge.game.zone.ZoneType;
import forge.item.PaperCard;
import forge.util.collect.FCollectionView;
import forge.util.MyRandom;
import forge.util.collect.FCollectionView;
import java.util.*;
import java.util.Map.Entry;
public class Match {
private final List<RegisteredPlayer> players;
@@ -243,8 +230,6 @@ public class Match {
}
}
player.initVariantsZones(psc);
Deck myDeck = psc.getDeck();
Set<PaperCard> myRemovedAnteCards = null;
@@ -263,8 +248,12 @@ public class Match {
if (myDeck.has(DeckSection.Sideboard)) {
preparePlayerLibrary(player, ZoneType.Sideboard, myDeck.get(DeckSection.Sideboard), psc.useRandomFoil(), generator);
}
player.initVariantsZones(psc);
player.shuffle(null);
if (isFirstGame) {
Collection<? extends PaperCard> cardsComplained = player.getController().complainCardsCantPlayWell(myDeck);
if (null != cardsComplained) {

View File

@@ -253,7 +253,9 @@ public final class AbilityFactory {
}
if (spellAbility instanceof SpellApiBased && hostCard.isPermanent()) {
spellAbility.setDescription(spellAbility.getHostCard().getName());
String desc = mapParams.containsKey("SpellDescription") ? mapParams.get("SpellDescription")
: spellAbility.getHostCard().getName();
spellAbility.setDescription(desc);
} else if (mapParams.containsKey("SpellDescription")) {
final StringBuilder sb = new StringBuilder();

View File

@@ -214,6 +214,11 @@ public class AbilityUtils {
if (o != null && o instanceof Card) {
cards.add(game.getCardState((Card) o));
}
} else if (defined.equals("LastRemembered")) {
Object o = Iterables.getLast(hostCard.getRemembered(), null);
if (o != null && o instanceof Card) {
cards.add(game.getCardState((Card) o));
}
} else if (defined.equals("Clones")) {
for (final Card clone : hostCard.getClones()) {
cards.add(game.getCardState(clone));
@@ -359,7 +364,7 @@ public class AbilityUtils {
if (StringUtils.isNumeric(amount)) {
int val = Integer.parseInt(amount);
if (maxto) {
val = Integer.max(val, 0);
val = Math.max(val, 0);
}
return val * multiplier;
}
@@ -399,7 +404,7 @@ public class AbilityUtils {
if (StringUtils.isNumeric(svarval)) {
int val = Integer.parseInt(svarval);
if (maxto) {
val = Integer.max(val, 0);
val = Math.max(val, 0);
}
return val * multiplier;
}
@@ -472,7 +477,7 @@ public class AbilityUtils {
if (val != null) {
if (maxto) {
val = Integer.max(val, 0);
val = Math.max(val, 0);
}
return val * multiplier;
}
@@ -971,10 +976,14 @@ public class AbilityUtils {
}
}
else if (defined.startsWith("Triggered")) {
String defParsed = defined.endsWith("AndYou") ? defined.substring(0, defined.indexOf("AndYou")) : defined;
if (defined.endsWith("AndYou")) {
players.add(sa.getActivatingPlayer());
}
final SpellAbility root = sa.getRootAbility();
Object o = null;
if (defined.endsWith("Controller")) {
String triggeringType = defined.substring(9);
if (defParsed.endsWith("Controller")) {
String triggeringType = defParsed.substring(9);
triggeringType = triggeringType.substring(0, triggeringType.length() - 10);
final Object c = root.getTriggeringObject(triggeringType);
if (c instanceof Card) {
@@ -984,8 +993,8 @@ public class AbilityUtils {
o = ((SpellAbility) c).getActivatingPlayer();
}
}
else if (defined.endsWith("Opponent")) {
String triggeringType = defined.substring(9);
else if (defParsed.endsWith("Opponent")) {
String triggeringType = defParsed.substring(9);
triggeringType = triggeringType.substring(0, triggeringType.length() - 8);
final Object c = root.getTriggeringObject(triggeringType);
if (c instanceof Card) {
@@ -995,8 +1004,8 @@ public class AbilityUtils {
o = ((SpellAbility) c).getActivatingPlayer().getOpponents();
}
}
else if (defined.endsWith("Owner")) {
String triggeringType = defined.substring(9);
else if (defParsed.endsWith("Owner")) {
String triggeringType = defParsed.substring(9);
triggeringType = triggeringType.substring(0, triggeringType.length() - 5);
final Object c = root.getTriggeringObject(triggeringType);
if (c instanceof Card) {
@@ -1004,7 +1013,7 @@ public class AbilityUtils {
}
}
else {
final String triggeringType = defined.substring(9);
final String triggeringType = defParsed.substring(9);
o = root.getTriggeringObject(triggeringType);
}
if (o != null) {
@@ -1302,7 +1311,11 @@ public class AbilityUtils {
// Needed - Equip an untapped creature with Sword of the Paruns then cast Deadshot on it. Should deal 2 more damage.
game.getAction().checkStaticAbilities(); // this will refresh continuous abilities for players and permanents.
game.getTriggerHandler().resetActiveTriggers();
if (sa.isReplacementAbility() && abSub.getApi() == ApiType.InternalEtbReplacement) {
game.getTriggerHandler().resetActiveTriggers(false);
} else {
game.getTriggerHandler().resetActiveTriggers();
}
AbilityUtils.resolveApiAbility(abSub, game);
}

View File

@@ -1,5 +1,6 @@
package forge.game.ability.effects;
import com.google.common.collect.Maps;
import forge.game.Game;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
@@ -8,6 +9,8 @@ import forge.game.spellability.SpellAbility;
import forge.game.trigger.TriggerType;
import forge.game.zone.ZoneType;
import java.util.Map;
public class AbandonEffect extends SpellAbilityEffect {
@@ -19,13 +22,27 @@ public class AbandonEffect extends SpellAbilityEffect {
Card source = sa.getHostCard();
Player controller = source.getController();
boolean isOptional = sa.hasParam("Optional");
if (isOptional && !controller.getController().confirmAction(sa, null, "Would you like to abandon the scheme " + source + "?")) {
return;
}
final Game game = controller.getGame();
if (sa.hasParam("RememberAbandoned")) {
source.addRemembered(source);
}
game.getTriggerHandler().suppressMode(TriggerType.ChangesZone);
controller.getZone(ZoneType.Command).remove(source);
game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone);
controller.getZone(ZoneType.SchemeDeck).add(source);
// Run triggers
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Scheme", source);
game.getTriggerHandler().runTrigger(TriggerType.Abandoned, runParams, false);
}
}

View File

@@ -19,8 +19,6 @@ import forge.util.Lang;
import java.util.List;
import com.google.common.collect.Iterables;
public class AttachEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {

View File

@@ -3,18 +3,14 @@ package forge.game.ability.effects;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
import forge.game.spellability.AbilitySub;
import forge.game.spellability.SpellAbility;
import forge.util.Expressions;
import java.util.List;
public class BranchEffect extends SpellAbilityEffect {
@Override
public void resolve(SpellAbility sa) {
final Card host = sa.getHostCard();
final Player player = host.getController();
// TODO Reuse SpellAbilityCondition and areMet() here instead of repeating each

View File

@@ -79,11 +79,11 @@ public class ChangeTextEffect extends SpellAbilityEffect {
validTypes.clear();
final List<String> forbiddenTypes = sa.hasParam("ForbiddenNewTypes") ? Lists.newArrayList(sa.getParam("ForbiddenNewTypes").split(",")) : Lists.<String>newArrayList();
forbiddenTypes.add(changedTypeWordOriginal);
if (changedTypeWordsArray[0].startsWith("Choose")) {
if (changedTypeWordsArray[0].equals("ChooseBasicLandType")) {
if (changedTypeWordsArray[1].startsWith("Choose")) {
if (changedTypeWordsArray[1].equals("ChooseBasicLandType")) {
validTypes.addAll(CardType.getBasicTypes());
kindOfType = "basic land";
} else if (changedTypeWordsArray[0].equals("ChooseCreatureType")) {
} else if (changedTypeWordsArray[1].equals("ChooseCreatureType")) {
validTypes.addAll(CardType.Constant.CREATURE_TYPES);
kindOfType = "creature";
}

View File

@@ -215,6 +215,8 @@ public class ChangeZoneAllEffect extends SpellAbilityEffect {
}
}
game.getTriggerHandler().resetActiveTriggers(false);
if (!triggerList.isEmpty()) {
final Map<String, Object> runParams = Maps.newHashMap();
runParams.put("Cards", triggerList);

View File

@@ -2,6 +2,7 @@ package forge.game.ability.effects;
import com.google.common.collect.Lists;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.card.Card;
import forge.game.player.Player;
@@ -104,10 +105,6 @@ public class CharmEffect extends SpellAbilityEffect {
sb.append(Lang.getNumeral(min)).append(" or ").append(list.size() == 2 ? "both" : "more");
}
if (repeat) {
sb.append(". You may choose the same mode more than once.");
}
if (sa.hasParam("ChoiceRestriction")) {
String rest = sa.getParam("ChoiceRestriction");
if (rest.equals("NotRemembered")) {
@@ -115,8 +112,17 @@ public class CharmEffect extends SpellAbilityEffect {
}
}
if (repeat) {
sb.append(". You may choose the same mode more than once.");
}
boolean additionalDesc = sa.hasParam("AdditionalDescription");
if (additionalDesc) {
sb.append(" ").append(sa.getParam("AdditionalDescription").trim());
}
if (!list.isEmpty()) {
if (!repeat) {
if (!repeat && !additionalDesc) {
sb.append(" \u2014");
}
sb.append("\r\n");
@@ -150,7 +156,9 @@ public class CharmEffect extends SpellAbilityEffect {
return;
}
final int num = Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
final int num = sa.hasParam("CharmNumOnResolve") ?
AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("CharmNumOnResolve"), sa)
: Integer.parseInt(sa.hasParam("CharmNum") ? sa.getParam("CharmNum") : "1");
final int min = sa.hasParam("MinCharmNum") ? Integer.parseInt(sa.getParam("MinCharmNum")) : num;
Card source = sa.getHostCard();
@@ -167,7 +175,7 @@ public class CharmEffect extends SpellAbilityEffect {
source.setChosenPlayer(chooser);
}
List<AbilitySub> chosen = chooser.getController().chooseModeForAbility(sa, min, num, sa.hasParam(("CanRepeatModes")));
List<AbilitySub> chosen = chooser.getController().chooseModeForAbility(sa, min, num, sa.hasParam("CanRepeatModes"));
chainAbilities(sa, chosen);
}

View File

@@ -63,6 +63,11 @@ public class CopySpellAbilityEffect extends SpellAbilityEffect {
controller = AbilityUtils.getDefinedPlayers(card, sa.getParam("Controller"), sa).get(0);
}
boolean isOptional = sa.hasParam("Optional");
if (isOptional && !controller.getController().confirmAction(sa, null, "Do you want to copy the spell " + card + "?")) {
return;
}
final List<SpellAbility> tgtSpells = getTargetSpells(sa);

View File

@@ -9,7 +9,6 @@ import forge.game.card.CardLists;
import forge.game.card.CounterType;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
public class CountersPutAllEffect extends SpellAbilityEffect {

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