From 836ebdc5e61b0f036a4b094d14e45ac2cf343c00 Mon Sep 17 00:00:00 2001 From: Sol Date: Sat, 8 Apr 2017 15:17:07 +0000 Subject: [PATCH] Initial checkin for Puzzle Mode --- .gitattributes | 9 + .../src/main/java/forge/ai/GameState.java | 286 +++++++++++++----- .../src/main/java/forge/deck/DeckFormat.java | 8 +- .../src/main/java/forge/game/GameAction.java | 57 +++- .../src/main/java/forge/game/GameEntity.java | 2 +- .../src/main/java/forge/game/GameType.java | 1 + .../src/main/java/forge/game/Match.java | 7 +- .../src/main/java/forge/game/card/Card.java | 2 +- .../java/forge/game/phase/PhaseHandler.java | 9 + .../main/java/forge/game/player/Player.java | 2 +- .../main/java/forge/gui/framework/EDocID.java | 4 + .../deckeditor/views/VCardCatalog.java | 6 +- .../java/forge/screens/home/EMenuGroup.java | 1 + .../main/java/forge/screens/home/VHomeUI.java | 6 + .../home/puzzle/CSubmenuPuzzleCreate.java | 37 +++ .../home/puzzle/CSubmenuPuzzleSolve.java | 113 +++++++ .../screens/home/puzzle/PuzzleGameMenu.java | 19 ++ .../home/puzzle/VSubmenuPuzzleCreate.java | 94 ++++++ .../home/puzzle/VSubmenuPuzzleSolve.java | 112 +++++++ .../src/forge/screens/home/NewGameMenu.java | 2 + .../screens/home/puzzle/PuzzleScreen.java | 42 +++ forge-gui/res/puzzle/PS1.pzl | 13 + .../main/java/forge/match/HostedMatch.java | 9 +- .../java/forge/properties/ForgeConstants.java | 2 + .../forge/properties/ForgePreferences.java | 1 + .../src/main/java/forge/puzzle/Puzzle.java | 104 +++++++ .../src/main/java/forge/puzzle/PuzzleIO.java | 50 +++ 27 files changed, 908 insertions(+), 90 deletions(-) create mode 100644 forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleCreate.java create mode 100644 forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleSolve.java create mode 100644 forge-gui-desktop/src/main/java/forge/screens/home/puzzle/PuzzleGameMenu.java create mode 100644 forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleCreate.java create mode 100644 forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleSolve.java create mode 100644 forge-gui-mobile/src/forge/screens/home/puzzle/PuzzleScreen.java create mode 100644 forge-gui/res/puzzle/PS1.pzl create mode 100644 forge-gui/src/main/java/forge/puzzle/Puzzle.java create mode 100644 forge-gui/src/main/java/forge/puzzle/PuzzleIO.java diff --git a/.gitattributes b/.gitattributes index 1134d24204c..2cc1c9fea85 100644 --- a/.gitattributes +++ b/.gitattributes @@ -993,6 +993,11 @@ forge-gui-desktop/src/main/java/forge/screens/home/online/CSubmenuOnlineLobby.ja forge-gui-desktop/src/main/java/forge/screens/home/online/OnlineMenu.java -text forge-gui-desktop/src/main/java/forge/screens/home/online/VSubmenuOnlineLobby.java -text forge-gui-desktop/src/main/java/forge/screens/home/package-info.java -text +forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleCreate.java -text +forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleSolve.java -text +forge-gui-desktop/src/main/java/forge/screens/home/puzzle/PuzzleGameMenu.java -text +forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleCreate.java -text +forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleSolve.java -text forge-gui-desktop/src/main/java/forge/screens/home/quest/CSubmenuChallenges.java -text forge-gui-desktop/src/main/java/forge/screens/home/quest/CSubmenuDuels.java -text forge-gui-desktop/src/main/java/forge/screens/home/quest/CSubmenuQuestData.java -text @@ -1347,6 +1352,7 @@ forge-gui-mobile/src/forge/screens/gauntlet/NewGauntletScreen.java -text forge-gui-mobile/src/forge/screens/home/HomeScreen.java -text forge-gui-mobile/src/forge/screens/home/LoadGameMenu.java -text forge-gui-mobile/src/forge/screens/home/NewGameMenu.java -text +forge-gui-mobile/src/forge/screens/home/puzzle/PuzzleScreen.java -text forge-gui-mobile/src/forge/screens/limited/DraftingProcessScreen.java -text forge-gui-mobile/src/forge/screens/limited/LoadDraftScreen.java -text forge-gui-mobile/src/forge/screens/limited/LoadSealedScreen.java -text @@ -18806,6 +18812,7 @@ 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/PS1.pzl -text forge-gui/res/quest/bazaar/ape_pet_l1.txt -text forge-gui/res/quest/bazaar/ape_pet_l2.txt -text forge-gui/res/quest/bazaar/ape_pet_l3.txt -text @@ -20844,6 +20851,8 @@ forge-gui/src/main/java/forge/player/package-info.java -text forge-gui/src/main/java/forge/properties/ForgeConstants.java -text forge-gui/src/main/java/forge/properties/ForgePreferences.java svneol=native#text/plain forge-gui/src/main/java/forge/properties/package-info.java svneol=native#text/plain +forge-gui/src/main/java/forge/puzzle/Puzzle.java -text +forge-gui/src/main/java/forge/puzzle/PuzzleIO.java -text forge-gui/src/main/java/forge/quest/BoosterUtils.java svneol=native#text/plain forge-gui/src/main/java/forge/quest/IQuestEvent.java -text forge-gui/src/main/java/forge/quest/IQuestRewardCard.java -text diff --git a/forge-ai/src/main/java/forge/ai/GameState.java b/forge-ai/src/main/java/forge/ai/GameState.java index b3ddb284065..3219460b17d 100644 --- a/forge-ai/src/main/java/forge/ai/GameState.java +++ b/forge-ai/src/main/java/forge/ai/GameState.java @@ -14,6 +14,7 @@ import com.google.common.collect.Lists; import forge.card.CardStateName; import forge.game.Game; +import forge.game.GameEntity; import forge.game.ability.effects.DetachedCardEffect; import forge.game.card.Card; import forge.game.card.CardCollection; @@ -31,7 +32,7 @@ import forge.util.collect.FCollectionView; public abstract class GameState { private static final Map ZONES = new HashMap(); static { - ZONES.put(ZoneType.Battlefield, "play"); + ZONES.put(ZoneType.Battlefield, "battlefield"); ZONES.put(ZoneType.Hand, "hand"); ZONES.put(ZoneType.Graveyard, "graveyard"); ZONES.put(ZoneType.Library, "library"); @@ -41,8 +42,15 @@ public abstract class GameState { private int humanLife = -1; private int computerLife = -1; + private String humanCounters = ""; + private String computerCounters = ""; + private final Map humanCardTexts = new EnumMap(ZoneType.class); private final Map aiCardTexts = new EnumMap(ZoneType.class); + + private final Map idToCard = new HashMap<>(); + private final Map cardToAttachId = new HashMap<>(); + private String tChangePlayer = "NONE"; private String tChangePhase = "NONE"; @@ -56,6 +64,14 @@ public abstract class GameState { StringBuilder sb = new StringBuilder(); sb.append(String.format("humanlife=%d\n", humanLife)); sb.append(String.format("ailife=%d\n", computerLife)); + + if (!humanCounters.isEmpty()) { + sb.append(String.format("humancounters=%s\n", humanCounters)); + } + if (!computerCounters.isEmpty()) { + sb.append(String.format("aicounters=%s\n", computerCounters)); + } + sb.append(String.format("activeplayer=%s\n", tChangePlayer)); sb.append(String.format("activephase=%s\n", tChangePhase)); appendCards(humanCardTexts, "human", sb); @@ -65,7 +81,7 @@ public abstract class GameState { private void appendCards(Map cardTexts, String categoryPrefix, StringBuilder sb) { for (Entry kv : cardTexts.entrySet()) { - sb.append(String.format("%scardsin%s=%s\n", categoryPrefix, ZONES.get(kv.getKey()), kv.getValue())); + sb.append(String.format("%s%s=%s\n", categoryPrefix, ZONES.get(kv.getKey()), kv.getValue())); } } @@ -82,6 +98,9 @@ public abstract class GameState { } humanLife = human.getLife(); computerLife = ai.getLife(); + humanCounters = countersToString(human.getCounters()); + computerCounters = countersToString(ai.getCounters()); + tChangePlayer = game.getPhaseHandler().getPlayerTurn() == ai ? "ai" : "human"; tChangePhase = game.getPhaseHandler().getPhase().toString(); aiCardTexts.clear(); @@ -111,43 +130,61 @@ public abstract class GameState { newText.append(c.getPaperCard().getName()); } if (c.isCommander()) { - newText.append("|IsCommander:True"); + newText.append("|IsCommander"); } if (zoneType == ZoneType.Battlefield) { if (c.isTapped()) { - newText.append("|Tapped:True"); + newText.append("|Tapped"); } if (c.isSick()) { - newText.append("|SummonSick:True"); + newText.append("|SummonSick"); } if (c.isFaceDown()) { - newText.append("|FaceDown:True"); + newText.append("|FaceDown"); + if (c.isManifested()) { + newText.append(":Manifested"); + } } Map counters = c.getCounters(); if (!counters.isEmpty()) { newText.append("|Counters:"); - boolean start = true; - for(Entry kv : counters.entrySet()) { - String str = kv.getKey().toString(); - int count = kv.getValue(); - for (int i = 0; i < count; i++) { - if (!start) { - newText.append(","); - } - newText.append(str); - start = false; - } - } + newText.append(countersToString(counters)); + } + if (c.getEquipping() != null) { + newText.append("|Attaching:").append(c.getEquipping().getId()); + } else if (c.getFortifying() != null) { + newText.append("|Attaching:").append(c.getFortifying().getId()); + } else if (c.getEnchantingCard() != null) { + 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()); } } cardTexts.put(zoneType, newText.toString()); } - private String[] parseLine(String line) { + private String countersToString(Map counters) { + boolean first = true; + StringBuilder counterString = new StringBuilder(); + + for(Entry kv : counters.entrySet()) { + if (!first) { + counterString.append(","); + } + + first = false; + counterString.append(String.format("%s=%d", kv.getKey().toString(), kv.getValue())); + } + return counterString.toString(); + } + + private String[] splitLine(String line) { if (line.charAt(0) == '#') { return null; } - final String[] tempData = line.split("="); + final String[] tempData = line.split("=", 2); if (tempData.length >= 2) { return tempData; } @@ -163,32 +200,91 @@ public abstract class GameState { String line; while ((line = br.readLine()) != null) { - String[] keyValue = parseLine(line); - if (keyValue == null) { - continue; - } - final String categoryName = keyValue[0].toLowerCase(); - final String categoryValue = keyValue[1]; + parseLine(line); + } + } - if (categoryName.equals("humanlife")) humanLife = Integer.parseInt(categoryValue); - else if (categoryName.equals("ailife")) computerLife = Integer.parseInt(categoryValue); + public void parse(List lines) { + for(String line : lines) { + parseLine(line); + } + } - else if (categoryName.equals("activeplayer")) tChangePlayer = categoryValue.trim().toLowerCase(); - else if (categoryName.equals("activephase")) tChangePhase = categoryValue; + protected void parseLine(String line) { + String[] keyValue = splitLine(line); + if (keyValue == null) return; - else if (categoryName.equals("humancardsinplay")) humanCardTexts.put(ZoneType.Battlefield, categoryValue); - else if (categoryName.equals("aicardsinplay")) aiCardTexts.put(ZoneType.Battlefield, categoryValue); - else if (categoryName.equals("humancardsinhand")) humanCardTexts.put(ZoneType.Hand, categoryValue); - else if (categoryName.equals("aicardsinhand")) aiCardTexts.put(ZoneType.Hand, categoryValue); - else if (categoryName.equals("humancardsingraveyard")) humanCardTexts.put(ZoneType.Graveyard, categoryValue); - else if (categoryName.equals("aicardsingraveyard")) aiCardTexts.put(ZoneType.Graveyard, categoryValue); - else if (categoryName.equals("humancardsinlibrary")) humanCardTexts.put(ZoneType.Library, categoryValue); - else if (categoryName.equals("aicardsinlibrary")) aiCardTexts.put(ZoneType.Library, categoryValue); - else if (categoryName.equals("humancardsinexile")) humanCardTexts.put(ZoneType.Exile, categoryValue); - else if (categoryName.equals("aicardsinexile")) aiCardTexts.put(ZoneType.Exile, categoryValue); - else if (categoryName.equals("humancardsincommand")) humanCardTexts.put(ZoneType.Command, categoryValue); - else if (categoryName.equals("aicardsincommand")) aiCardTexts.put(ZoneType.Command, categoryValue); - else System.out.println("Unknown key: " + categoryName); + final String categoryName = keyValue[0].toLowerCase(); + final String categoryValue = keyValue[1]; + + if (categoryName.startsWith("active")) { + if (categoryName.endsWith("player")) + tChangePlayer = categoryValue.trim().toLowerCase(); + if (categoryName.endsWith("phase")) + tChangePhase = categoryValue.trim().toUpperCase(); + return; + } + + boolean isHuman = categoryName.startsWith("human"); + + if (categoryName.endsWith("life")) { + if (isHuman) + humanLife = Integer.parseInt(categoryValue); + else + computerLife = Integer.parseInt(categoryValue); + } + + else if (categoryName.endsWith("counters")) { + if (isHuman) + humanCounters = categoryValue; + else + computerCounters = categoryValue; + } + + else if (categoryName.endsWith("play") || categoryName.endsWith("battlefield")) { + if (isHuman) + humanCardTexts.put(ZoneType.Battlefield, categoryValue); + else + aiCardTexts.put(ZoneType.Battlefield, categoryValue); + } + + else if (categoryName.endsWith("hand")) { + if (isHuman) + humanCardTexts.put(ZoneType.Hand, categoryValue); + else + aiCardTexts.put(ZoneType.Hand, categoryValue); + } + + else if (categoryName.endsWith("graveyard")) { + if (isHuman) + humanCardTexts.put(ZoneType.Graveyard, categoryValue); + else + aiCardTexts.put(ZoneType.Graveyard, categoryValue); + } + + else if (categoryName.equals("library")) { + if (isHuman) + humanCardTexts.put(ZoneType.Library, categoryValue); + else + aiCardTexts.put(ZoneType.Library, categoryValue); + } + + else if (categoryName.equals("exile")) { + if (isHuman) + humanCardTexts.put(ZoneType.Exile, categoryValue); + else + aiCardTexts.put(ZoneType.Exile, categoryValue); + } + + else if (categoryName.equals("command")) { + if (isHuman) + humanCardTexts.put(ZoneType.Command, categoryValue); + else + aiCardTexts.put(ZoneType.Command, categoryValue); + } + + else { + System.out.println("Unknown key: " + categoryName); } } @@ -196,27 +292,54 @@ public abstract class GameState { game.getAction().invoke(new Runnable() { @Override public void run() { - final Player human = game.getPlayers().get(0); - final Player ai = game.getPlayers().get(1); - - Player newPlayerTurn = tChangePlayer.equals("human") ? newPlayerTurn = human : tChangePlayer.equals("ai") ? newPlayerTurn = ai : null; - PhaseType newPhase = tChangePhase.trim().equalsIgnoreCase("none") ? null : PhaseType.smartValueOf(tChangePhase); - - game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn); - - game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); - - setupPlayerState(humanLife, humanCardTexts, human); - setupPlayerState(computerLife, aiCardTexts, ai); - - game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); - - game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated + applyGameOnThread(game); } }); } + protected void applyGameOnThread(final Game game) { + final Player human = game.getPlayers().get(0); + final Player ai = game.getPlayers().get(1); + + Player newPlayerTurn = tChangePlayer.equals("human") ? human : tChangePlayer.equals("ai") ? ai : null; + PhaseType newPhase = tChangePhase.equals("none") ? null : PhaseType.smartValueOf(tChangePhase); + + // Set stack to resolving so things won't trigger/effects be checked right away + game.getStack().setResolving(true); + + if (!humanCounters.isEmpty()) { + applyCountersToGameEntity(human, humanCounters); + } + if (!computerCounters.isEmpty()) { + applyCountersToGameEntity(ai, computerCounters); + } + game.getPhaseHandler().devModeSet(newPhase, newPlayerTurn); + + game.getTriggerHandler().suppressMode(TriggerType.ChangesZone); + + setupPlayerState(humanLife, humanCardTexts, human); + setupPlayerState(computerLife, aiCardTexts, ai); + + game.getTriggerHandler().clearSuppression(TriggerType.ChangesZone); + + game.getStack().setResolving(false); + + game.getAction().checkStateEffects(true); //ensure state based effects and triggers are updated + } + + private void applyCountersToGameEntity(GameEntity entity, String counterString) { + entity.setCounters(new HashMap()); + String[] allCounterStrings = counterString.split(","); + for (final String counterPair : allCounterStrings) { + String[] pair = counterPair.split("=", 2); + + entity.addCounter(CounterType.valueOf(pair[0]), Integer.parseInt(pair[1]), false, false); + } + } + private void setupPlayerState(int life, Map cardTexts, final Player p) { + // Lock check static as we setup player state + Map playerCards = new EnumMap(ZoneType.class); for (Entry kv : cardTexts.entrySet()) { String value = kv.getValue(); @@ -245,7 +368,12 @@ public abstract class GameState { // var as-is. c.setCounters(new HashMap()); p.getZone(ZoneType.Hand).add(c); - p.getGame().getAction().moveToPlay(c); + if (c.isAura()) { + p.getGame().getAction().moveToPlay(c); + } else { + p.getGame().getAction().moveToPlay(c); + } + c.setTapped(tapped); c.setSickness(sickness); c.setCounters(counters); @@ -254,6 +382,19 @@ public abstract class GameState { zone.setCards(kv.getValue()); } } + + for(Entry 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); + } + } } /** @@ -265,7 +406,7 @@ public abstract class GameState { * an array of {@link java.lang.String} objects. * @param player * a {@link forge.game.player.Player} object. - * @return a {@link forge.CardList} object. + * @return a {@link CardCollectionView} object. */ private CardCollectionView processCardsForZone(final String[] data, final Player player) { final CardCollection cl = new CardCollection(); @@ -286,22 +427,29 @@ public abstract class GameState { if (info.startsWith("Set:")) { c.setSetCode(info.substring(info.indexOf(':') + 1)); hasSetCurSet = true; - } else if (info.equalsIgnoreCase("Tapped:True")) { + } + else if (info.startsWith("Tapped")) { c.tap(); } else if (info.startsWith("Counters:")) { - final String[] counterStrings = info.substring(info.indexOf(':') + 1).split(","); - for (final String counter : counterStrings) { - c.addCounter(CounterType.valueOf(counter), 1, true); - } - } else if (info.equalsIgnoreCase("SummonSick:True")) { + applyCountersToGameEntity(c, info.substring(info.indexOf(':') + 1)); + } else if (info.startsWith("SummonSick")) { c.setSickness(true); - } else if (info.equalsIgnoreCase("FaceDown:True")) { + } else if (info.startsWith("FaceDown")) { c.setState(CardStateName.FaceDown, true); - } else if (info.equalsIgnoreCase("IsCommander:True")) { + if (info.endsWith("Manifested")) { + c.setManifested(true); + } + } else if (info.startsWith("IsCommander")) { // TODO: This doesn't seem to properly restore the ability to play the commander. Why? c.setCommander(true); player.setCommanders(Lists.newArrayList(c)); player.getZone(ZoneType.Command).add(Player.createCommanderEffect(player.getGame(), c)); + } else if (info.startsWith("Id:")) { + int id = Integer.parseInt(info.substring(4)); + idToCard.put(id, c); + } else if (info.startsWith("Attaching:")) { + int id = Integer.parseInt(info.substring(info.indexOf(':') + 1)); + cardToAttachId.put(c, id); } } diff --git a/forge-core/src/main/java/forge/deck/DeckFormat.java b/forge-core/src/main/java/forge/deck/DeckFormat.java index 094d6c391cb..8aa8ee5709b 100644 --- a/forge-core/src/main/java/forge/deck/DeckFormat.java +++ b/forge-core/src/main/java/forge/deck/DeckFormat.java @@ -94,13 +94,15 @@ public enum DeckFormat { PlanarConquest ( Range.between(40, Integer.MAX_VALUE), Range.is(0), 1), Vanguard ( Range.between(60, Integer.MAX_VALUE), Range.is(0), 4), Planechase ( Range.between(60, Integer.MAX_VALUE), Range.is(0), 4), - Archenemy ( Range.between(60, Integer.MAX_VALUE), Range.is(0), 4); + Archenemy ( Range.between(60, Integer.MAX_VALUE), Range.is(0), 4), + Puzzle ( Range.between(0, Integer.MAX_VALUE), Range.is(0), 4); private final Range mainRange; private final Range sideRange; // null => no check private final int maxCardCopies; private final Predicate cardPoolFilter; private final static String ADVPROCLAMATION = "Advantageous Proclamation"; + private final static String SOVREALM = "Sovereign's Realm"; private DeckFormat(Range mainRange0, Range sideRange0, int maxCardCopies0) { this(mainRange0, sideRange0, maxCardCopies0, null); @@ -168,11 +170,13 @@ public enum DeckFormat { int min = getMainRange().getMinimum(); int max = getMainRange().getMaximum(); + boolean noBasicLands = false; // Adjust minimum base on number of Advantageous Proclamation or similar cards CardPool conspiracies = deck.get(DeckSection.Conspiracy); if (conspiracies != null) { - min -= (5 * conspiracies.countByName(ADVPROCLAMATION, true)); + min -= (5 * conspiracies.countByName(ADVPROCLAMATION, false)); + noBasicLands = conspiracies.countByName(SOVREALM, false) > 0; } if (hasCommander()) { // 1 Commander, or 2 Partner Commanders diff --git a/forge-game/src/main/java/forge/game/GameAction.java b/forge-game/src/main/java/forge/game/GameAction.java index 07549b0398c..81b3f5d9392 100644 --- a/forge-game/src/main/java/forge/game/GameAction.java +++ b/forge-game/src/main/java/forge/game/GameAction.java @@ -17,6 +17,7 @@ */ package forge.game; +import com.google.common.base.Predicate; import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; @@ -1434,6 +1435,10 @@ public class GameAction { } public void startGame(GameOutcome lastGameOutcome) { + startGame(lastGameOutcome, null); + } + + public void startGame(GameOutcome lastGameOutcome, Runnable startGameHook) { Player first = determineFirstTurnPlayer(lastGameOutcome); GameType gameType = game.getRules().getGameType(); @@ -1444,6 +1449,9 @@ public class GameAction { // Where there are none, it should bring up speed controls game.fireEvent(new GameEventGameStarted(gameType, first, game.getPlayers())); + // Emissary's Plot + // runPreOpeningHandActions(first); + game.setAge(GameStage.Mulligan); for (final Player p1 : game.getPlayers()) { p1.drawCards(p1.getMaxHandSize()); @@ -1453,8 +1461,9 @@ public class GameAction { } // Choose starting hand for each player with multiple hands - - performMulligans(first, game.getRules().hasAppliedVariant(GameType.Commander)); + if (game.getRules().getGameType() != GameType.Puzzle) { + performMulligans(first, game.getRules().hasAppliedVariant(GameType.Commander)); + } if (game.isGameOver()) { break; } // conceded during "mulligan" prompt game.setAge(GameStage.Play); @@ -1472,8 +1481,8 @@ public class GameAction { game.getTriggerHandler().runTrigger(TriggerType.NewGame, runParams, true); // - game.getPhaseHandler().startFirstTurn(first); + game.getPhaseHandler().startFirstTurn(first, startGameHook); //after game ends, ensure Auto-Pass canceled for all players so it doesn't apply to next game for (Player p : game.getRegisteredPlayers()) { p.getController().autoPassCancel(); @@ -1487,15 +1496,20 @@ public class GameAction { // Only cut/coin toss if it's the first game of the match Player goesFirst = null; - // 904.6: in Archenemy games the Archenemy goes first - if (game != null && game.getRules().hasAppliedVariant(GameType.Archenemy)) { - for (Player p : game.getPlayers()) { - if (p.isArchenemy()) { - return p; + if (game != null) { + if (game.getRules().getGameType().equals(GameType.Puzzle)) { + return game.getPlayers().get(0); + } + + // 904.6: in Archenemy games the Archenemy goes first + if (game.getRules().hasAppliedVariant(GameType.Archenemy)) { + for (Player p : game.getPlayers()) { + if (p.isArchenemy()) { + return p; + } } } } - // Power Play - Each player with a Power Play in the CommandZone becomes the Starting Player Set powerPlayers = Sets.newHashSet(); for (Card c : game.getCardsIn(ZoneType.Command)) { @@ -1629,6 +1643,31 @@ public class GameAction { } } + private void runPreOpeningHandActions(final Player first) { + Player takesAction = first; + do { + // + List ploys = CardLists.filter(takesAction.getCardsIn(ZoneType.Command), new Predicate() { + @Override + public boolean apply(Card input) { + return input.getName().equals("Emissary's Ploy"); + } + }); + + int chosen = 1; + List cmc = Lists.newArrayList(1, 2, 3); + + for (Card c : ploys) { + if (!cmc.isEmpty()) { + chosen = takesAction.getController().chooseNumber(c.getSpellPermanent(), "Emissary's Ploy", cmc, c.getOwner()); + cmc.remove((Object)chosen); + } + + c.setChosenNumber(chosen); + } + takesAction = game.getNextPlayerAfter(takesAction); + } while (takesAction != first); + } private void runOpeningHandActions(final Player first) { Player takesAction = first; diff --git a/forge-game/src/main/java/forge/game/GameEntity.java b/forge-game/src/main/java/forge/game/GameEntity.java index 0fe4bc60509..0a4b1ccac95 100644 --- a/forge-game/src/main/java/forge/game/GameEntity.java +++ b/forge-game/src/main/java/forge/game/GameEntity.java @@ -354,7 +354,7 @@ public abstract class GameEntity extends GameObject implements IIdentifiable { abstract public void setCounters(final Map allCounters); abstract public boolean canReceiveCounters(final CounterType type); - abstract protected void addCounter(final CounterType counterType, final int n, final boolean applyMultiplier, final boolean fireEvents); + abstract public void addCounter(final CounterType counterType, final int n, final boolean applyMultiplier, final boolean fireEvents); abstract public void subtractCounter(final CounterType counterName, final int n); abstract public void clearCounters(); diff --git a/forge-game/src/main/java/forge/game/GameType.java b/forge-game/src/main/java/forge/game/GameType.java index 4fc99f1f9d4..8dac5f78c1e 100644 --- a/forge-game/src/main/java/forge/game/GameType.java +++ b/forge-game/src/main/java/forge/game/GameType.java @@ -17,6 +17,7 @@ public enum GameType { Quest (DeckFormat.QuestDeck, true, true, false, "Quest", ""), QuestDraft (DeckFormat.Limited, true, true, true, "Quest Draft", ""), PlanarConquest (DeckFormat.PlanarConquest, true, false, false, "Planar Conquest", ""), + Puzzle (DeckFormat.Puzzle, false, false, false, "Puzzle", "Solve a puzzle from the given game state"), Constructed (DeckFormat.Constructed, false, true, true, "Constructed", ""), DeckManager (DeckFormat.Constructed, false, true, true, "Deck Manager", ""), Vanguard (DeckFormat.Vanguard, true, true, true, "Vanguard", "Each player has a special \"Avatar\" card that affects the game."), diff --git a/forge-game/src/main/java/forge/game/Match.java b/forge-game/src/main/java/forge/game/Match.java index 816fd23fe84..09cf4555d87 100644 --- a/forge-game/src/main/java/forge/game/Match.java +++ b/forge-game/src/main/java/forge/game/Match.java @@ -80,6 +80,10 @@ public class Match { } public void startGame(final Game game) { + startGame(game, null); + } + + public void startGame(final Game game, Runnable startGameHook) { prepareAllZones(game); if (rules.useAnte()) { // Deciding which cards go to ante Multimap list = game.chooseCardsForAnte(rules.getMatchAnteRarity()); @@ -92,7 +96,8 @@ public class Match { } GameOutcome lastOutcome = gamesPlayed.isEmpty() ? null : gamesPlayed.get(gamesPlayed.size() - 1); - game.getAction().startGame(lastOutcome); + + game.getAction().startGame(lastOutcome, startGameHook); if (rules.useAnte()) { executeAnte(game); diff --git a/forge-game/src/main/java/forge/game/card/Card.java b/forge-game/src/main/java/forge/game/card/Card.java index 42a6eddd0fc..db0cf8c8d18 100644 --- a/forge-game/src/main/java/forge/game/card/Card.java +++ b/forge-game/src/main/java/forge/game/card/Card.java @@ -987,7 +987,7 @@ public class Card extends GameEntity implements Comparable { } @Override - protected void addCounter(final CounterType counterType, final int n, final boolean applyMultiplier, final boolean fireEvents) { + public void addCounter(final CounterType counterType, final int n, final boolean applyMultiplier, final boolean fireEvents) { int addAmount = n; if(addAmount < 0) { addAmount = 0; // As per rule 107.1b diff --git a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java index 3526de9c181..dc311796bab 100644 --- a/forge-game/src/main/java/forge/game/phase/PhaseHandler.java +++ b/forge-game/src/main/java/forge/game/phase/PhaseHandler.java @@ -878,6 +878,10 @@ public class PhaseHandler implements java.io.Serializable { private final static boolean DEBUG_PHASES = false; public void startFirstTurn(Player goesFirst) { + startFirstTurn(goesFirst, null); + } + + public void startFirstTurn(Player goesFirst, Runnable startGameHook) { StopWatch sw = new StopWatch(); if (phase != null) { @@ -891,6 +895,11 @@ public class PhaseHandler implements java.io.Serializable { // don't even offer priority, because it's untap of 1st turn now givePriorityToPlayer = false; + if (startGameHook != null) { + startGameHook.run(); + givePriorityToPlayer = true; + } + // MAIN GAME LOOP while (!game.isGameOver()) { if (givePriorityToPlayer) { diff --git a/forge-game/src/main/java/forge/game/player/Player.java b/forge-game/src/main/java/forge/game/player/Player.java index ea98c2874b0..f1cab9d28e2 100644 --- a/forge-game/src/main/java/forge/game/player/Player.java +++ b/forge-game/src/main/java/forge/game/player/Player.java @@ -884,7 +884,7 @@ public class Player extends GameEntity implements Comparable { } @Override - protected void addCounter(CounterType counterType, int n, boolean applyMultiplier, boolean fireEvents) { + public void addCounter(CounterType counterType, int n, boolean applyMultiplier, boolean fireEvents) { if (!canReceiveCounters(counterType)) { return; } diff --git a/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java b/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java index 65e43c12aaa..4bdfa8c1b6c 100644 --- a/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java +++ b/forge-gui-desktop/src/main/java/forge/gui/framework/EDocID.java @@ -16,6 +16,8 @@ import forge.screens.home.gauntlet.VSubmenuGauntletContests; import forge.screens.home.gauntlet.VSubmenuGauntletLoad; import forge.screens.home.gauntlet.VSubmenuGauntletQuick; import forge.screens.home.online.VSubmenuOnlineLobby; +import forge.screens.home.puzzle.VSubmenuPuzzleCreate; +import forge.screens.home.puzzle.VSubmenuPuzzleSolve; import forge.screens.home.quest.VSubmenuChallenges; import forge.screens.home.quest.VSubmenuDuels; import forge.screens.home.quest.VSubmenuQuestData; @@ -71,6 +73,8 @@ public enum EDocID { HOME_ACHIEVEMENTS (VSubmenuAchievements.SINGLETON_INSTANCE), HOME_AVATARS (VSubmenuAvatars.SINGLETON_INSTANCE), HOME_UTILITIES (VSubmenuDownloaders.SINGLETON_INSTANCE), + HOME_PUZZLE_CREATE(VSubmenuPuzzleCreate.SINGLETON_INSTANCE), + HOME_PUZZLE_SOLVE(VSubmenuPuzzleSolve.SINGLETON_INSTANCE), HOME_CONSTRUCTED (VSubmenuConstructed.SINGLETON_INSTANCE), HOME_DRAFT (VSubmenuDraft.SINGLETON_INSTANCE), HOME_SEALED (VSubmenuSealed.SINGLETON_INSTANCE), diff --git a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCardCatalog.java b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCardCatalog.java index ff64dfab452..e7233bd40bc 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCardCatalog.java +++ b/forge-gui-desktop/src/main/java/forge/screens/deckeditor/views/VCardCatalog.java @@ -1,8 +1,5 @@ package forge.screens.deckeditor.views; -import javax.swing.JPanel; - -import net.miginfocom.swing.MigLayout; import forge.gui.framework.DragCell; import forge.gui.framework.DragTab; import forge.gui.framework.EDocID; @@ -11,6 +8,9 @@ import forge.item.InventoryItem; import forge.itemmanager.ItemManager; import forge.itemmanager.ItemManagerContainer; import forge.screens.deckeditor.controllers.CCardCatalog; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; /** * Assembles Swing components of card catalog in deck editor. diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/EMenuGroup.java b/forge-gui-desktop/src/main/java/forge/screens/home/EMenuGroup.java index 7bad96f2bd2..9f7e6ebafed 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/EMenuGroup.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/EMenuGroup.java @@ -11,6 +11,7 @@ public enum EMenuGroup { SANCTIONED ("Sanctioned Formats"), ONLINE ("Online Multiplayer"), QUEST ("Quest Mode"), + PUZZLE ("Puzzle Mode"), GAUNTLET ("Gauntlets"), SETTINGS ("Game Settings"); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/VHomeUI.java b/forge-gui-desktop/src/main/java/forge/screens/home/VHomeUI.java index 38ad108cedd..a806e0600c2 100644 --- a/forge-gui-desktop/src/main/java/forge/screens/home/VHomeUI.java +++ b/forge-gui-desktop/src/main/java/forge/screens/home/VHomeUI.java @@ -32,6 +32,8 @@ import javax.swing.JPanel; import javax.swing.ScrollPaneConstants; import javax.swing.SwingConstants; +import forge.screens.home.puzzle.VSubmenuPuzzleCreate; +import forge.screens.home.puzzle.VSubmenuPuzzleSolve; import net.miginfocom.swing.MigLayout; import forge.Singletons; import forge.assets.FSkinProp; @@ -56,6 +58,7 @@ import forge.screens.home.quest.VSubmenuQuestPrefs; import forge.screens.home.sanctioned.VSubmenuConstructed; import forge.screens.home.sanctioned.VSubmenuDraft; import forge.screens.home.sanctioned.VSubmenuSealed; +import forge.screens.home.sanctioned.VSubmenuWinston; import forge.screens.home.settings.VSubmenuAchievements; import forge.screens.home.settings.VSubmenuAvatars; import forge.screens.home.settings.VSubmenuDownloaders; @@ -132,6 +135,9 @@ public enum VHomeUI implements IVTopLevelUI { allSubmenus.add(VSubmenuGauntletLoad.SINGLETON_INSTANCE); allSubmenus.add(VSubmenuGauntletContests.SINGLETON_INSTANCE); + allSubmenus.add(VSubmenuPuzzleSolve.SINGLETON_INSTANCE); + allSubmenus.add(VSubmenuPuzzleCreate.SINGLETON_INSTANCE); + allSubmenus.add(VSubmenuPreferences.SINGLETON_INSTANCE); allSubmenus.add(VSubmenuAchievements.SINGLETON_INSTANCE); allSubmenus.add(VSubmenuAvatars.SINGLETON_INSTANCE); diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleCreate.java b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleCreate.java new file mode 100644 index 00000000000..55edfa27ff4 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleCreate.java @@ -0,0 +1,37 @@ +package forge.screens.home.puzzle; + +import forge.gui.framework.ICDoc; +import forge.menus.IMenuProvider; +import forge.menus.MenuUtil; + +import javax.swing.*; +import java.util.ArrayList; +import java.util.List; + +public enum CSubmenuPuzzleCreate implements ICDoc, IMenuProvider { + SINGLETON_INSTANCE; + + private VSubmenuPuzzleCreate view = VSubmenuPuzzleCreate.SINGLETON_INSTANCE; + + @Override + public void register() { + + } + + @Override + public void initialize() { + + } + + @Override + public void update() { + MenuUtil.setMenuProvider(this); + } + + @Override + public List getMenus() { + final List menus = new ArrayList(); + menus.add(PuzzleGameMenu.getMenu()); + return menus; + } +} diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleSolve.java b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleSolve.java new file mode 100644 index 00000000000..6a1966649cf --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/CSubmenuPuzzleSolve.java @@ -0,0 +1,113 @@ +package forge.screens.home.puzzle; + +import forge.GuiBase; +import forge.UiCommand; +import forge.deck.Deck; +import forge.game.GameRules; +import forge.game.GameType; +import forge.game.player.RegisteredPlayer; +import forge.gauntlet.GauntletData; +import forge.gauntlet.GauntletIO; +import forge.gui.SOverlayUtils; +import forge.gui.framework.ICDoc; +import forge.match.HostedMatch; +import forge.menus.IMenuProvider; +import forge.menus.MenuUtil; +import forge.player.GamePlayerUtil; +import forge.puzzle.Puzzle; +import forge.puzzle.PuzzleIO; +import forge.quest.QuestUtil; + +import javax.swing.*; +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; +import java.io.File; +import java.util.ArrayList; +import java.util.List; + +public enum CSubmenuPuzzleSolve implements ICDoc, IMenuProvider { + SINGLETON_INSTANCE; + + private VSubmenuPuzzleSolve view = VSubmenuPuzzleSolve.SINGLETON_INSTANCE; + + @Override + public void register() { + + } + + @Override + public void initialize() { + view.getList().setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + updateData(); + view.getBtnStart().addActionListener( + new ActionListener() { @Override + public void actionPerformed(final ActionEvent e) { startPuzzleSolve(); } }); + } + + private final UiCommand cmdStart = new UiCommand() { + @Override public void run() { + startPuzzleSolve(); + } + }; + + private void updateData() { + final ArrayList puzzles = PuzzleIO.loadPuzzles(); + for(Puzzle p : puzzles) { + view.getModel().addElement(p); + } + } + + @Override + public void update() { + MenuUtil.setMenuProvider(this); + } + + @Override + public List getMenus() { + final List menus = new ArrayList(); + menus.add(PuzzleGameMenu.getMenu()); + return menus; + } + + private boolean startPuzzleSolve() { + final Puzzle selected = (Puzzle)view.getList().getSelectedValue(); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + SOverlayUtils.startGameOverlay(); + SOverlayUtils.showOverlay(); + } + }); + + final HostedMatch hostedMatch = GuiBase.getInterface().hostMatch(); + hostedMatch.setStartGameHook(new Runnable() { + @Override + public final void run() { + selected.applyToGame(hostedMatch.getGame()); + } + }); + + final List players = new ArrayList(); + final RegisteredPlayer human = new RegisteredPlayer(new Deck()).setPlayer(GamePlayerUtil.getGuiPlayer()); + human.setStartingHand(0); + players.add(human); + + final RegisteredPlayer ai = new RegisteredPlayer(new Deck()).setPlayer(GamePlayerUtil.createAiPlayer()); + ai.setStartingHand(0); + players.add(ai); + + GameRules rules = new GameRules(GameType.Puzzle); + rules.setGamesPerMatch(1); + hostedMatch.startMatch(rules, null, players, human, GuiBase.getInterface().getNewGuiGame()); + + SwingUtilities.invokeLater(new Runnable() { + @Override + public void run() { + SOverlayUtils.hideOverlay(); + } + }); + + return true; + } +} diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/PuzzleGameMenu.java b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/PuzzleGameMenu.java new file mode 100644 index 00000000000..8f584449f2f --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/PuzzleGameMenu.java @@ -0,0 +1,19 @@ +package forge.screens.home.puzzle; + +import forge.model.FModel; +import forge.properties.ForgePreferences; + +import javax.swing.*; +import java.awt.event.KeyEvent; + +public class PuzzleGameMenu { + private PuzzleGameMenu() { } + + private static ForgePreferences prefs = FModel.getPreferences(); + + public static JMenu getMenu() { + JMenu menu = new JMenu("Puzzle"); + menu.setMnemonic(KeyEvent.VK_G); + return menu; + } +} diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleCreate.java b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleCreate.java new file mode 100644 index 00000000000..2208ac186da --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleCreate.java @@ -0,0 +1,94 @@ +package forge.screens.home.puzzle; + +import forge.gui.framework.DragCell; +import forge.gui.framework.DragTab; +import forge.gui.framework.EDocID; +import forge.interfaces.IPlayerChangeListener; +import forge.match.GameLobby; +import forge.match.LocalLobby; +import forge.net.event.UpdateLobbyPlayerEvent; +import forge.screens.home.EMenuGroup; +import forge.screens.home.IVSubmenu; +import forge.screens.home.VHomeUI; +import forge.screens.home.VLobby; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; + +public enum VSubmenuPuzzleCreate implements IVSubmenu { + SINGLETON_INSTANCE; + + private DragCell parentCell; + private final DragTab tab = new DragTab("Puzzle Mode: Create"); + + private final GameLobby lobby = new LocalLobby(); + private final VLobby vLobby = new VLobby(lobby); + + VSubmenuPuzzleCreate() { + lobby.setListener(vLobby); + + vLobby.setPlayerChangeListener(new IPlayerChangeListener() { + @Override public final void update(final int index, final UpdateLobbyPlayerEvent event) { + lobby.applyToSlot(index, event); + } + }); + + vLobby.update(false); + } + + @Override + public EMenuGroup getGroupEnum() { + return EMenuGroup.PUZZLE; + } + + @Override + public String getMenuTitle() { + return "Create"; + } + + @Override + public EDocID getItemEnum() { + return EDocID.HOME_PUZZLE_CREATE; + } + + @Override + public EDocID getDocumentID() { + return EDocID.HOME_PUZZLE_CREATE; + } + + @Override + public DragTab getTabLabel() { + return tab; + } + + @Override + public CSubmenuPuzzleCreate getLayoutControl() { + return CSubmenuPuzzleCreate.SINGLETON_INSTANCE; + } + + @Override + public void setParentCell(DragCell cell0) { + this.parentCell = cell0; + } + + @Override + public DragCell getParentCell() { + return this.parentCell; + } + + @Override + public void populate() { + final JPanel container = VHomeUI.SINGLETON_INSTANCE.getPnlDisplay(); + + container.removeAll(); + container.setLayout(new MigLayout("insets 0, gap 0, wrap 1, ax right")); + vLobby.getLblTitle().setText("Puzzle Mode: Create"); + container.add(vLobby.getLblTitle(), "w 80%, h 40px!, gap 0 0 15px 15px, span 2, al right, pushx"); + + + if (container.isShowing()) { + container.validate(); + container.repaint(); + } + } +} diff --git a/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleSolve.java b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleSolve.java new file mode 100644 index 00000000000..c711a60da70 --- /dev/null +++ b/forge-gui-desktop/src/main/java/forge/screens/home/puzzle/VSubmenuPuzzleSolve.java @@ -0,0 +1,112 @@ +package forge.screens.home.puzzle; + +import forge.gui.framework.DragCell; +import forge.gui.framework.DragTab; +import forge.gui.framework.EDocID; +import forge.interfaces.IPlayerChangeListener; +import forge.match.GameLobby; +import forge.match.LocalLobby; +import forge.net.event.UpdateLobbyPlayerEvent; +import forge.screens.home.*; +import net.miginfocom.swing.MigLayout; + +import javax.swing.*; + +public enum VSubmenuPuzzleSolve implements IVSubmenu { + SINGLETON_INSTANCE; + + private final JList puzzleList; + final DefaultListModel model = new DefaultListModel(); + + private final StartButton btnStart = new StartButton(); + + private DragCell parentCell; + private final DragTab tab = new DragTab("Puzzle Mode: Solve"); + + private final GameLobby lobby = new LocalLobby(); + private final VLobby vLobby = new VLobby(lobby); + + VSubmenuPuzzleSolve() { + puzzleList = new JList<>(); + lobby.setListener(vLobby); + + vLobby.setPlayerChangeListener(new IPlayerChangeListener() { + @Override public final void update(final int index, final UpdateLobbyPlayerEvent event) { + lobby.applyToSlot(index, event); + } + }); + + vLobby.update(false); + } + + @Override + public EMenuGroup getGroupEnum() { + return EMenuGroup.PUZZLE; + } + + @Override + public String getMenuTitle() { + return "Solve"; + } + + @Override + public EDocID getItemEnum() { + return EDocID.HOME_PUZZLE_SOLVE; + } + + @Override + public EDocID getDocumentID() { + return EDocID.HOME_PUZZLE_SOLVE; + } + + @Override + public DragTab getTabLabel() { + return tab; + } + + @Override + public CSubmenuPuzzleSolve getLayoutControl() { + return CSubmenuPuzzleSolve.SINGLETON_INSTANCE; + } + + @Override + public void setParentCell(DragCell cell0) { + this.parentCell = cell0; + } + + @Override + public DragCell getParentCell() { + return this.parentCell; + } + + public JList getList() { + return puzzleList; + } + + public DefaultListModel getModel() { + return model; + } + + public StartButton getBtnStart() { + return btnStart; + } + + @Override + public void populate() { + final JPanel container = VHomeUI.SINGLETON_INSTANCE.getPnlDisplay(); + + container.removeAll(); + container.setLayout(new MigLayout("insets 0, gap 0, wrap 1, ax right")); + vLobby.getLblTitle().setText("Puzzle Mode: Solve"); + container.add(vLobby.getLblTitle(), "w 80%, h 40px!, gap 0 0 15px 15px, span 2, al right, pushx"); + puzzleList.setModel(model); + container.add(puzzleList, "w 80%, h 200px!, gap 0 0 0px 0px, span 2, al center"); + container.add(btnStart, "w 98%!, ax center, gap 1% 0 20px 20px, span 2"); + + + if (container.isShowing()) { + container.validate(); + container.repaint(); + } + } +} diff --git a/forge-gui-mobile/src/forge/screens/home/NewGameMenu.java b/forge-gui-mobile/src/forge/screens/home/NewGameMenu.java index 6933bce6373..f392d0df8f6 100644 --- a/forge-gui-mobile/src/forge/screens/home/NewGameMenu.java +++ b/forge-gui-mobile/src/forge/screens/home/NewGameMenu.java @@ -11,6 +11,7 @@ import forge.properties.ForgePreferences.FPref; import forge.screens.FScreen; import forge.screens.constructed.ConstructedScreen; import forge.screens.gauntlet.NewGauntletScreen; +import forge.screens.home.puzzle.PuzzleScreen; import forge.screens.limited.NewDraftScreen; import forge.screens.limited.NewSealedScreen; import forge.screens.planarconquest.NewConquestScreen; @@ -24,6 +25,7 @@ public class NewGameMenu extends FPopupMenu { BoosterDraft("Booster Draft", FSkinImage.HAND, NewDraftScreen.class), SealedDeck("Sealed Deck", FSkinImage.PACK, NewSealedScreen.class), QuestMode("Quest Mode", FSkinImage.QUEST_ZEP, NewQuestScreen.class), + PuzzleMode("Puzzle Mode", FSkinImage.QUEST_BOOK, PuzzleScreen.class), PlanarConquest("Planar Conquest", FSkinImage.MULTIVERSE, NewConquestScreen.class), Gauntlet("Gauntlet", FSkinImage.ALPHASTRIKE, NewGauntletScreen.class); diff --git a/forge-gui-mobile/src/forge/screens/home/puzzle/PuzzleScreen.java b/forge-gui-mobile/src/forge/screens/home/puzzle/PuzzleScreen.java new file mode 100644 index 00000000000..3396183fbe8 --- /dev/null +++ b/forge-gui-mobile/src/forge/screens/home/puzzle/PuzzleScreen.java @@ -0,0 +1,42 @@ +package forge.screens.home.puzzle; + +import forge.assets.FSkinFont; +import forge.screens.LaunchScreen; +import forge.screens.home.NewGameMenu; +import forge.toolbox.FLabel; +import forge.toolbox.FTextArea; +import forge.util.ThreadUtil; +import forge.util.Utils; + +public class PuzzleScreen extends LaunchScreen { + private static final float PADDING = Utils.scale(10); + + private final FTextArea lblDesc = add(new FTextArea(false, + "Puzzle Mode loads in a puzzle that you have to win in a predetermined time/way.")); + + public PuzzleScreen() { + super(null, NewGameMenu.getMenu()); + + lblDesc.setFont(FSkinFont.get(12)); + lblDesc.setTextColor(FLabel.INLINE_LABEL_COLOR); + } + + @Override + protected void doLayoutAboveBtnStart(float startY, float width, float height) { + float x = PADDING; + float y = startY + PADDING; + float w = width - 2 * PADDING; + float h = height - y - PADDING; + lblDesc.setBounds(x, y, w, h); + } + + @Override + protected void startMatch() { + ThreadUtil.invokeInGameThread(new Runnable() { //must run in game thread to prevent blocking UI thread + @Override + public void run() { + // Load selected puzzle + } + }); + } +} diff --git a/forge-gui/res/puzzle/PS1.pzl b/forge-gui/res/puzzle/PS1.pzl new file mode 100644 index 00000000000..d1fb1ee4479 --- /dev/null +++ b/forge-gui/res/puzzle/PS1.pzl @@ -0,0 +1,13 @@ +[metadata] +Name:Possibility Storm #1 +URL:https://i.redd.it/wws1h03gy7ky.png +Goal:Win +Turns:1 +[state] +ActivePlayer=Human +ActivePhase=Main1 +HumanLife=20 +AILife=9 +HumanPlay=Key to the City; Ravenous Intruder; Ruinous Gremlin; Island; Island; Island; Island; Island; Mountain; Mountain +HumanHand=Welcome to the Fold; Chilling Grasp; Flame Lash; Confirm Suspicions +AIPlay=Workshop Assistant; Thriving Ibex \ No newline at end of file diff --git a/forge-gui/src/main/java/forge/match/HostedMatch.java b/forge-gui/src/main/java/forge/match/HostedMatch.java index ecf723e5395..9205904d32a 100644 --- a/forge-gui/src/main/java/forge/match/HostedMatch.java +++ b/forge-gui/src/main/java/forge/match/HostedMatch.java @@ -54,6 +54,7 @@ public class HostedMatch { private Match match; private Game game; private String title; + private Runnable startGameHook = null; private final List humanControllers = Lists.newArrayList(); private Map guis; private int humanCount; @@ -62,7 +63,10 @@ public class HostedMatch { private final Map nextGameDecisions = Maps.newHashMap(); private boolean isMatchOver = false; - public HostedMatch() { + public HostedMatch() {} + + public void setStartGameHook(Runnable hook) { + startGameHook = hook; } private static GameRules getDefaultRules(final GameType gameType) { @@ -216,9 +220,8 @@ public class HostedMatch { playbackControl.setGame(game); game.subscribeToEvents(playbackControl); } - // Actually start the game! - match.startGame(game); + match.startGame(game, startGameHook); // After game is over... isMatchOver = match.isMatchOver(); diff --git a/forge-gui/src/main/java/forge/properties/ForgeConstants.java b/forge-gui/src/main/java/forge/properties/ForgeConstants.java index b184f48212a..ccda66c64bd 100644 --- a/forge-gui/src/main/java/forge/properties/ForgeConstants.java +++ b/forge-gui/src/main/java/forge/properties/ForgeConstants.java @@ -62,6 +62,8 @@ public final class ForgeConstants { public static final String MUSIC_DIR = RES_DIR + "music" + PATH_SEPARATOR; public static final String LANG_DIR = RES_DIR + "languages" + PATH_SEPARATOR; public static final String EFFECTS_DIR = RES_DIR + "effects" + PATH_SEPARATOR; + public static final String PUZZLE_DIR = RES_DIR + "puzzle" + PATH_SEPARATOR; + private static final String QUEST_DIR = RES_DIR + "quest" + PATH_SEPARATOR; public static final String QUEST_WORLD_DIR = QUEST_DIR + "world" + PATH_SEPARATOR; diff --git a/forge-gui/src/main/java/forge/properties/ForgePreferences.java b/forge-gui/src/main/java/forge/properties/ForgePreferences.java index e51bec3e366..bfd9033876a 100644 --- a/forge-gui/src/main/java/forge/properties/ForgePreferences.java +++ b/forge-gui/src/main/java/forge/properties/ForgePreferences.java @@ -108,6 +108,7 @@ public class ForgePreferences extends PreferencesStore { SUBMENU_ONLINE ("false"), SUBMENU_GAUNTLET ("false"), SUBMENU_QUEST ("false"), + SUBMENU_PUZZLE("false"), SUBMENU_SETTINGS ("false"), SUBMENU_UTILITIES ("false"), diff --git a/forge-gui/src/main/java/forge/puzzle/Puzzle.java b/forge-gui/src/main/java/forge/puzzle/Puzzle.java new file mode 100644 index 00000000000..15e4e56250f --- /dev/null +++ b/forge-gui/src/main/java/forge/puzzle/Puzzle.java @@ -0,0 +1,104 @@ +package forge.puzzle; + +import forge.ai.GameState; +import forge.game.Game; +import forge.game.ability.AbilityFactory; +import forge.game.card.Card; +import forge.game.player.Player; +import forge.game.trigger.Trigger; +import forge.game.trigger.TriggerHandler; +import forge.game.zone.PlayerZone; +import forge.game.zone.ZoneType; +import forge.item.IPaperCard; +import forge.item.InventoryItem; +import forge.model.FModel; + +import java.util.List; +import java.util.Map; + +public class Puzzle extends GameState implements InventoryItem { + String name; + String goal; + String url; + int turns; + + public Puzzle(Map> puzzleLines) { + loadMetaData(puzzleLines.get("metadata")); + loadGameState(puzzleLines.get("state")); + // Generate goal enforcement + } + + private void loadMetaData(List metadataLines) { + for(String line : metadataLines) { + String[] split = line.split(":"); + if ("Name".equalsIgnoreCase(split[0])) { + this.name = split[1]; + } else if ("Goal".equalsIgnoreCase(split[0])) { + this.goal = split[1]; + } else if ("Url".equalsIgnoreCase(split[0])) { + this.url = split[1]; + } else if ("Turns".equalsIgnoreCase(split[0])) { + this.turns = Integer.parseInt(split[1]); + } + } + } + + private void loadGameState(List stateLines) { + this.parse(stateLines); + } + + public IPaperCard getPaperCard(final String cardName) { + return FModel.getMagicDb().getCommonCards().getCard(cardName); + } + + public void addGoalEnforcement(Game game) { + Player human = null; + for(Player p : game.getPlayers()) { + if (p.getController().isGuiPlayer()) { + human = p; + } + } + + Card goalCard = new Card(-1, game); + + goalCard.setOwner(human); + goalCard.setImageKey("t:puzzle"); + goalCard.setName("Puzzle Goal"); + goalCard.addType("Effect"); + + { + final String loseTrig = "Mode$ Phase | Phase$ Cleanup | TriggerZones$ Command | Static$ True | " + + "ValidPlayer$ You | TriggerDescription$ At the beginning of your cleanup step, you lose the game."; + final String loseEff = "DB$ LosesGame | Defined$ You"; + + final Trigger loseTrigger = TriggerHandler.parseTrigger(loseTrig, goalCard, true); + + loseTrigger.setOverridingAbility(AbilityFactory.getAbility(loseEff, goalCard)); + goalCard.addTrigger(loseTrigger); + } + human.getZone(ZoneType.Command).add(goalCard); + } + + @Override + protected void applyGameOnThread(final Game game) { + super.applyGameOnThread(game); + addGoalEnforcement(game); + } + + @Override + public String getItemType() { + return "Puzzle"; + } + + @Override + public String getImageKey(boolean altState) { + return null; + } + + @Override + public String getName() { + return name; + } + + public String toString() { return name; } +} diff --git a/forge-gui/src/main/java/forge/puzzle/PuzzleIO.java b/forge-gui/src/main/java/forge/puzzle/PuzzleIO.java new file mode 100644 index 00000000000..4313d16cf77 --- /dev/null +++ b/forge-gui/src/main/java/forge/puzzle/PuzzleIO.java @@ -0,0 +1,50 @@ +package forge.puzzle; + +import com.google.common.collect.Lists; +import forge.properties.ForgeConstants; +import forge.util.FileSection; +import forge.util.FileUtil; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class PuzzleIO { + + public static final String TXF_PROMPT = "[New Puzzle]"; + public static final String SUFFIX_DATA = ".pzl"; + + public static ArrayList loadPuzzles() { + String[] pList; + // get list of custom draft files + final File pFolder = new File(ForgeConstants.PUZZLE_DIR); + if (!pFolder.exists()) { + throw new RuntimeException("Puzzles : folder not found -- folder is " + pFolder.getAbsolutePath()); + } + + if (!pFolder.isDirectory()) { + throw new RuntimeException("Puzzles : not a folder -- " + pFolder.getAbsolutePath()); + } + + pList = pFolder.list(); + + ArrayList puzzles = Lists.newArrayList(); + for (final String element : pList) { + if (element.endsWith(SUFFIX_DATA)) { + final List pfData = FileUtil.readFile(ForgeConstants.PUZZLE_DIR + element); + puzzles.add(new Puzzle(parsePuzzleSections(pfData))); + } + } + return puzzles; + } + + public static final Map> parsePuzzleSections(List pfData) { + return FileSection.parseSections(pfData); + } + + + public static File getPuzzleFile(final String name) { + return new File(ForgeConstants.PUZZLE_DIR, name + SUFFIX_DATA); + } +}