From 6b64667767af3dfe7f4cb0cc81467330ed5e9a23 Mon Sep 17 00:00:00 2001 From: Seravy Date: Sun, 18 Feb 2018 21:37:20 +0100 Subject: [PATCH 1/8] Do not spend more on X than the number of lands we can discard! --- forge-ai/src/main/java/forge/ai/ability/DestroyAi.java | 10 ++++++++++ forge-gui/res/cardsfolder/s/scorched_earth.txt | 4 ++-- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index 9770e0e756d..6cdde3c38a1 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -169,6 +169,16 @@ public class DestroyAi extends SpellAbilityAi { if (hasXCost) { // TODO: currently the AI will maximize mana spent on X, trying to maximize damage. This may need improvement. maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa)); + // X can't be more than the lands we have in our hand for "discard X lands"! + if ("ScorchedEarth".equals(logic)) { + int lands = 0; + for (Card c : ai.getCardsIn(ZoneType.Hand)) { + if (c.isLand()) { + lands++; + } + } + maxTargets = Math.min(maxTargets, lands); + } } if (sa.hasParam("AIMaxTgtsCount")) { // Cards that have confusing costs for the AI (e.g. Eliminate the Competition) can have forced max target constraints specified diff --git a/forge-gui/res/cardsfolder/s/scorched_earth.txt b/forge-gui/res/cardsfolder/s/scorched_earth.txt index 2f2744c16a1..fc6bc2c5f8d 100644 --- a/forge-gui/res/cardsfolder/s/scorched_earth.txt +++ b/forge-gui/res/cardsfolder/s/scorched_earth.txt @@ -1,10 +1,10 @@ Name:Scorched Earth ManaCost:X R Types:Sorcery -A:SP$ Destroy | Cost$ X R Discard | TargetMin$ 0 | TargetMax$ MaxTgts | ValidTgts$ Land | TgtPrompt$ Select target land | References$ X | SpellDescription$ Destroy X target lands. +A:SP$ Destroy | Cost$ X R Discard | TargetMin$ 0 | TargetMax$ MaxTgts | ValidTgts$ Land | TgtPrompt$ Select target land | References$ X | SpellDescription$ Destroy X target lands. | AILogic$ ScorchedEarth # It may seem wrong to not use X in the target, but since the Targets are what defines X, it's redundant (and not supported by the code) SVar:X:Targeted$Amount SVar:MaxTgts:Count$Valid Land -SVar:RemAIDeck:True +SVar:RemRandomDeck:True SVar:Picture:http://www.wizards.com/global/images/magic/general/scorched_earth.jpg Oracle:As an additional cost to cast Scorched Earth, discard X land cards.\nDestroy X target lands. From 70593c2b6a401dfe8e7df40731f7373d07786435 Mon Sep 17 00:00:00 2001 From: Seravy Date: Sun, 18 Feb 2018 23:35:44 +0100 Subject: [PATCH 2/8] Try to play Scorching Earth before playing a land for higher X. --- forge-gui/res/cardsfolder/s/scorched_earth.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/forge-gui/res/cardsfolder/s/scorched_earth.txt b/forge-gui/res/cardsfolder/s/scorched_earth.txt index fc6bc2c5f8d..d67b59385df 100644 --- a/forge-gui/res/cardsfolder/s/scorched_earth.txt +++ b/forge-gui/res/cardsfolder/s/scorched_earth.txt @@ -6,5 +6,6 @@ A:SP$ Destroy | Cost$ X R Discard | TargetMin$ 0 | TargetMax$ MaxTgts | SVar:X:Targeted$Amount SVar:MaxTgts:Count$Valid Land SVar:RemRandomDeck:True +SVar:PlayBeforeLandDrop:true SVar:Picture:http://www.wizards.com/global/images/magic/general/scorched_earth.jpg Oracle:As an additional cost to cast Scorched Earth, discard X land cards.\nDestroy X target lands. From 1d470f87d48c98d914c77756c86c01f716e2e540 Mon Sep 17 00:00:00 2001 From: Seravy Date: Mon, 19 Feb 2018 12:55:08 +0100 Subject: [PATCH 3/8] Gerrard's Battle Cry : play before combat --- forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt b/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt index d510eff60af..b1b6cbabc98 100644 --- a/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt +++ b/forge-gui/res/cardsfolder/g/gerrards_battle_cry.txt @@ -3,5 +3,6 @@ ManaCost:W Types:Enchantment A:AB$ PumpAll | Cost$ 2 W | ValidCards$ Creature.YouCtrl | NumAtt$ +1 | NumDef$ +1 | SpellDescription$ Creatures you control get +1/+1 until end of turn. SVar:NonStackingEffect:True +SVar:PlayMain1:TRUE SVar:Picture:http://www.wizards.com/global/images/magic/general/gerrards_battle_cry.jpg Oracle:{2}{W}: Creatures you control get +1/+1 until end of turn. From f229fbc4b189ce8b150c6f8e116f38805285e90f Mon Sep 17 00:00:00 2001 From: Seravy Date: Mon, 19 Feb 2018 12:54:16 +0000 Subject: [PATCH 4/8] Update DestroyAi.java --- forge-ai/src/main/java/forge/ai/ability/DestroyAi.java | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java index 6cdde3c38a1..57c30d33851 100644 --- a/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java +++ b/forge-ai/src/main/java/forge/ai/ability/DestroyAi.java @@ -171,12 +171,7 @@ public class DestroyAi extends SpellAbilityAi { maxTargets = Math.min(ComputerUtilMana.determineMaxAffordableX(ai, sa), abTgt.getMaxTargets(sa.getHostCard(), sa)); // X can't be more than the lands we have in our hand for "discard X lands"! if ("ScorchedEarth".equals(logic)) { - int lands = 0; - for (Card c : ai.getCardsIn(ZoneType.Hand)) { - if (c.isLand()) { - lands++; - } - } + int lands = CardLists.filter(ai.getCardsIn(ZoneType.Hand), CardPredicates.Presets.LANDS).size(); maxTargets = Math.min(maxTargets, lands); } } From 8d2d66abb98267bce1ce3bd0f240151fcfb85771 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Thu, 15 Feb 2018 18:07:22 -0700 Subject: [PATCH 5/8] move external address out to a class attribute Signed-off-by: Jamin W. Collins --- forge-gui/src/main/java/forge/net/server/FServerManager.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/forge-gui/src/main/java/forge/net/server/FServerManager.java b/forge-gui/src/main/java/forge/net/server/FServerManager.java index 8b8416c8a04..2d9118764e5 100644 --- a/forge-gui/src/main/java/forge/net/server/FServerManager.java +++ b/forge-gui/src/main/java/forge/net/server/FServerManager.java @@ -55,6 +55,7 @@ import com.google.common.collect.Maps; public final class FServerManager { private static FServerManager instance = null; + private byte[] externalAddress = new byte[]{8,8,8,8}; private boolean isHosting = false; private final EventLoopGroup bossGroup = new NioEventLoopGroup(1); private final EventLoopGroup workerGroup = new NioEventLoopGroup(); @@ -210,7 +211,7 @@ public final class FServerManager { // https://stackoverflow.com/a/901943 private String getRoutableAddress(boolean preferIpv4, boolean preferIPv6) throws SocketException, UnknownHostException { DatagramSocket s = new DatagramSocket(); - s.connect(InetAddress.getByAddress(new byte[]{8,8,8,8}), 0); + s.connect(InetAddress.getByAddress(this.externalAddress), 0); NetworkInterface n = NetworkInterface.getByInetAddress(s.getLocalAddress()); Enumeration en = n.getInetAddresses(); while (en.hasMoreElements()) { From d086e4d6926ef79aaf432859433bcbc04681e657 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Sun, 18 Feb 2018 19:50:03 -0700 Subject: [PATCH 6/8] cleanup remote client game state creation Signed-off-by: Jamin W. Collins --- .../forge/net/client/GameClientHandler.java | 66 +++++++++++-------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/forge-gui/src/main/java/forge/net/client/GameClientHandler.java b/forge-gui/src/main/java/forge/net/client/GameClientHandler.java index f15a4576bc5..08955a4d2c1 100644 --- a/forge-gui/src/main/java/forge/net/client/GameClientHandler.java +++ b/forge-gui/src/main/java/forge/net/client/GameClientHandler.java @@ -27,6 +27,8 @@ final class GameClientHandler extends GameProtocolHandler { private final FGameClient client; private final IGuiGame gui; private Tracker tracker; + private Match match; + private Game game; /** * Creates a client-side game handler. @@ -36,6 +38,8 @@ final class GameClientHandler extends GameProtocolHandler { this.client = client; this.gui = client.getGui(); this.tracker = null; + this.match = null; + this.game = null; } @Override @@ -58,32 +62,26 @@ final class GameClientHandler extends GameProtocolHandler { protected void beforeCall(final ProtocolMethod protocolMethod, final Object[] args) { switch (protocolMethod) { case openView: - if (this.tracker == null) { - int maxAttempts = 5; - for (int numAttempts = 0; numAttempts < maxAttempts; numAttempts++) { - try { + // only need one **match** + if (this.match == null) { + this.match = createMatch(); + } - this.tracker = createTracker(); + // openView is called **once** per game, for now create a new Game instance each time + this.game = createGame(); - for (PlayerView myPlayer : (TrackableCollection) args[0]) { - if (myPlayer.getTracker() == null) { - myPlayer.setTracker(this.tracker); - } - } + // get a tracker + this.tracker = createTracker(); - final TrackableCollection myPlayers = (TrackableCollection) args[0]; - client.setGameControllers(myPlayers); - - } catch (Exception e) { - System.err.println("Failed: attempt number: " + numAttempts + " - " + e.toString()); - try { - Thread.sleep(100); - } catch (InterruptedException e1) { - e1.printStackTrace(); - } - } + for (PlayerView myPlayer : (TrackableCollection) args[0]) { + if (myPlayer.getTracker() == null) { + myPlayer.setTracker(this.tracker); } } + + final TrackableCollection myPlayers = (TrackableCollection) args[0]; + client.setGameControllers(myPlayers); + break; default: break; @@ -108,19 +106,17 @@ final class GameClientHandler extends GameProtocolHandler { } /** - * This method creates the necessary objects and state to retrieve a Tracker object. - * - * Near as I can tell, that means that we need to create a Match. + * This method retrieves enough of the existing (incomplete) game state to + * recreate a new viable Match object * * Creating a Match requires that we have: * * GameRules * * RegisteredPlayers * * Title * - * @return Tracker + * @return Match */ - private Tracker createTracker() { - + private Match createMatch() { // retrieve what we can from the existing (but incomplete) state final IGuiGame gui = client.getGui(); GameView gameView = gui.getGameView(); @@ -134,12 +130,24 @@ final class GameClientHandler extends GameProtocolHandler { // create a valid match object and game Match match = new Match(gameRules, registeredPlayers, title); - Game game = match.createGame(); + return match; + } + + private Game createGame() { + this.tracker = null; + return this.match.createGame(); + } + + /** + * Ensure the stored GameView is correct and retrieve a Tracker object. + * + * @return Tracker + */ + private Tracker createTracker() { // replace the existing incomplete GameView with the newly created one gui.setGameView(null); gui.setGameView(game.getView()); - return gui.getGameView().getTracker(); } From b25bc72f3e9e6581f7670e83b48f7c5e87db3792 Mon Sep 17 00:00:00 2001 From: Hanmac Date: Mon, 19 Feb 2018 21:37:16 +0100 Subject: [PATCH 7/8] CardFactory: fixed keywords for SplitCards --- .../src/main/java/forge/game/card/CardFactory.java | 7 ++++++- .../main/java/forge/game/card/CardFactoryUtil.java | 12 +++++------- .../src/main/java/forge/game/card/CardState.java | 3 --- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/forge-game/src/main/java/forge/game/card/CardFactory.java b/forge-game/src/main/java/forge/game/card/CardFactory.java index 66fc64cfcbd..012b6effe00 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactory.java +++ b/forge-game/src/main/java/forge/game/card/CardFactory.java @@ -295,9 +295,10 @@ public class CardFactory { sa.setRightSplit(); } } + CardFactoryUtil.setupKeywordedAbilities(card); final CardState original = card.getState(CardStateName.Original); original.addNonManaAbilities(card.getCurrentState().getNonManaAbilities()); - original.addIntrinsicKeywords(card.getCurrentState().getIntrinsicKeywordStrings(), false); // Copy 'Fuse' to original side + original.addIntrinsicKeywords(card.getCurrentState().getIntrinsicKeywords()); // Copy 'Fuse' to original side original.getSVars().putAll(card.getCurrentState().getSVars()); // Unfortunately need to copy these to (Effect looks for sVars on execute) } else if (state != CardStateName.Original){ CardFactoryUtil.setupKeywordedAbilities(card); @@ -305,6 +306,10 @@ public class CardFactory { } card.setState(CardStateName.Original, false); + // need to update keyword cache for original spell + if (card.isSplitCard()) { + card.updateKeywordsCache(card.getCurrentState()); + } // ****************************************************************** // ************** Link to different CardFactories ******************* diff --git a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java index 022c5e0be99..a090562c9a2 100644 --- a/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java +++ b/forge-game/src/main/java/forge/game/card/CardFactoryUtil.java @@ -2999,7 +2999,7 @@ public class CardFactoryUtil { public static void addReplacementEffect(final KeywordInterface inst, final Card card, final boolean intrinsic) { String keyword = inst.getOriginal(); - if (keyword.equals("Aftermath")) { + if (keyword.equals("Aftermath") && card.getCurrentStateName().equals(CardStateName.RightSplit)) { StringBuilder sb = new StringBuilder(); sb.append("Event$ Moved | ValidCard$ Card.Self | Origin$ Stack | ExcludeDestination$ Exile "); sb.append("| ValidStackSa$ Spell.Aftermath | Description$ Aftermath"); @@ -3021,8 +3021,6 @@ public class CardFactoryUtil { re.setOverridingAbility(saExile); - // Aftermath only on Rightsplit - // doesn't make a copy with it inst.addReplacement(re); } else if (keyword.startsWith("Amplify")) { final String[] ampString = keyword.split(":"); @@ -3493,11 +3491,11 @@ public class CardFactoryUtil { inst.addSpellAbility(newSA); } - } else if (keyword.equals("Aftermath")) { + } else if (keyword.equals("Aftermath") && card.getCurrentStateName().equals(CardStateName.RightSplit)) { // Aftermath does modify existing SA, and does not add new one // only target RightSplit of it - final SpellAbility origSA = card.getState(CardStateName.RightSplit).getFirstAbility(); + final SpellAbility origSA = card.getFirstSpellAbility(); origSA.setAftermath(true); origSA.getRestrictions().setZone(ZoneType.Graveyard); // The Exile part is done by the System itself @@ -3742,9 +3740,9 @@ public class CardFactoryUtil { inst.addSpellAbility(sa); - } else if (keyword.startsWith("Fuse")) { + } else if (keyword.startsWith("Fuse") && card.getCurrentStateName().equals(CardStateName.Original)) { final SpellAbility sa = AbilityFactory.buildFusedAbility(card); - card.getState(CardStateName.Original).addNonManaAbility(sa); + card.addSpellAbility(sa); sa.setTemporary(!intrinsic); inst.addSpellAbility(sa); diff --git a/forge-game/src/main/java/forge/game/card/CardState.java b/forge-game/src/main/java/forge/game/card/CardState.java index c481ad2c234..d3f7d38698f 100644 --- a/forge-game/src/main/java/forge/game/card/CardState.java +++ b/forge-game/src/main/java/forge/game/card/CardState.java @@ -164,9 +164,6 @@ public class CardState extends GameObject { public final Collection getIntrinsicKeywords() { return intrinsicKeywords.getValues(); } - public final Iterable getIntrinsicKeywordStrings() { - return intrinsicKeywords; - } public final boolean hasIntrinsicKeyword(String k) { return intrinsicKeywords.contains(k); } From 487fec02580e981842de103bd7a2549ec3602984 Mon Sep 17 00:00:00 2001 From: "Jamin W. Collins" Date: Mon, 19 Feb 2018 13:11:53 -0700 Subject: [PATCH 8/8] multiplayer fix for 2nd and 3rd games of match The server was not updating the lobby player's game reference. So, the remote client's response was being processed for the wrong game instance. Signed-off-by: Jamin W. Collins --- .../src/main/java/forge/match/GameLobby.java | 6 ++++- .../main/java/forge/match/HostedMatch.java | 22 +++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/forge-gui/src/main/java/forge/match/GameLobby.java b/forge-gui/src/main/java/forge/match/GameLobby.java index 168a331c60b..097705ed545 100644 --- a/forge-gui/src/main/java/forge/match/GameLobby.java +++ b/forge-gui/src/main/java/forge/match/GameLobby.java @@ -4,10 +4,12 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collections; import java.util.EnumSet; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; + import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -49,7 +51,7 @@ public abstract class GameLobby implements IHasGameType { private final boolean allowNetworking; private HostedMatch hostedMatch; - private final Map gameControllers = Maps.newHashMap(); + private final HashMap gameControllers = Maps.newHashMap(); protected GameLobby(final boolean allowNetworking) { this.allowNetworking = allowNetworking; } @@ -481,6 +483,8 @@ public abstract class GameLobby implements IHasGameType { } } + hostedMatch.gameControllers = gameControllers; + onGameStarted(); } }; diff --git a/forge-gui/src/main/java/forge/match/HostedMatch.java b/forge-gui/src/main/java/forge/match/HostedMatch.java index 3e3f757a962..d42aaa68072 100644 --- a/forge-gui/src/main/java/forge/match/HostedMatch.java +++ b/forge-gui/src/main/java/forge/match/HostedMatch.java @@ -3,11 +3,14 @@ package forge.match; import java.util.Collection; import java.util.Collections; import java.util.Comparator; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Set; +import forge.LobbyPlayer; +import forge.interfaces.IGameController; import forge.util.TextUtil; import org.apache.commons.lang3.StringUtils; @@ -55,6 +58,7 @@ public class HostedMatch { private Match match; private Game game; private String title; + public HashMap gameControllers = null; private Runnable startGameHook = null; private final List humanControllers = Lists.newArrayList(); private Map guis; @@ -180,6 +184,12 @@ public class HostedMatch { game.subscribeToEvents(new FControlGameEventHandler(humanController)); playersPerGui.add(gui, p.getView()); + + if (gameControllers != null ) { + LobbySlot lobbySlot = getLobbySlot(p.getLobbyPlayer()); + gameControllers.put(lobbySlot, humanController); + } + humanControllers.add(humanController); humanCount++; } @@ -238,6 +248,18 @@ public class HostedMatch { }); } + private LobbySlot getLobbySlot(LobbyPlayer lobbyPlayer) { + for (LobbySlot key: gameControllers.keySet()) { + IGameController value = gameControllers.get(key); + if (value instanceof PlayerControllerHuman) { + if (lobbyPlayer == ((PlayerControllerHuman) value).getLobbyPlayer()) { + return key; + } + } + } + return null; + } + public void registerSpectator(final IGuiGame gui) { final PlayerControllerHuman humanController = new WatchLocalGame(game, null, gui); gui.setSpectator(humanController);