Merge branch 'surveil' into 'master'

Surveil

Closes #681 and #684

See merge request core-developers/forge!904
This commit is contained in:
Jamin Collins
2018-09-08 15:22:28 +00:00
19 changed files with 249 additions and 24 deletions

View File

@@ -292,8 +292,26 @@ public class PlayerControllerAi extends PlayerController {
return ImmutablePair.of(toTop, toBottom);
}
/* (non-Javadoc)
* @see forge.game.player.PlayerController#arrangeForSurveil(forge.game.card.CardCollection)
*/
@Override
public ImmutablePair<CardCollection, CardCollection> arrangeForSurveil(CardCollection topN) {
CardCollection toGraveyard = new CardCollection();
CardCollection toTop = new CardCollection();
// TODO add AI logic there, similar to Scry
toTop.addAll(topN);
Collections.shuffle(toTop, MyRandom.getRandom());
return ImmutablePair.of(toTop, toGraveyard);
}
@Override
public boolean willPutCardOnTop(Card c) {
// TODO add Logic there similar to Scry. this is used for Clash
return true; // AI does not know what will happen next (another clash or that would become his topdeck)
}

View File

@@ -145,6 +145,7 @@ public enum SpellApiToAi {
.put(ApiType.SkipTurn, SkipTurnAi.class)
.put(ApiType.StoreMap, StoreMapAi.class)
.put(ApiType.StoreSVar, StoreSVarAi.class)
.put(ApiType.Surveil, SurveilAi.class)
.put(ApiType.Tap, TapAi.class)
.put(ApiType.TapAll, TapAllAi.class)
.put(ApiType.TapOrUntap, TapOrUntapAi.class)

View File

@@ -11,7 +11,6 @@ import forge.game.phase.PhaseType;
import forge.game.player.Player;
import forge.game.player.PlayerActionConfirmMode;
import forge.game.spellability.SpellAbility;
import forge.game.spellability.TargetRestrictions;
import forge.game.zone.ZoneType;
import forge.util.MyRandom;
@@ -22,9 +21,8 @@ public class ScryAi extends SpellAbilityAi {
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
final TargetRestrictions tgt = sa.getTargetRestrictions();
if (tgt != null) { // It doesn't appear that Scry ever targets
if (sa.usesTargeting()) { // It doesn't appear that Scry ever targets
// ability is targeted
sa.resetTargets();

View File

@@ -0,0 +1,44 @@
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 SurveilAi extends SpellAbilityAi {
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#doTriggerAINoCost(forge.game.player.Player, forge.game.spellability.SpellAbility, boolean)
*/
@Override
protected boolean doTriggerAINoCost(Player ai, SpellAbility sa, boolean mandatory) {
if (sa.usesTargeting()) { // It doesn't appear that Scry ever targets
// ability is targeted
sa.resetTargets();
sa.getTargets().add(ai);
}
return true;
}
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#chkAIDrawback(forge.game.spellability.SpellAbility, forge.game.player.Player)
*/
@Override
public boolean chkAIDrawback(SpellAbility sa, Player ai) {
return doTriggerAINoCost(ai, sa, false);
}
/*
* (non-Javadoc)
* @see forge.ai.SpellAbilityAi#confirmAction(forge.game.player.Player, forge.game.spellability.SpellAbility, forge.game.player.PlayerActionConfirmMode, java.lang.String)
*/
@Override
public boolean confirmAction(Player player, SpellAbility sa, PlayerActionConfirmMode mode, String message) {
return true;
}
}

View File

@@ -1848,7 +1848,7 @@ public class GameAction {
//Vancouver Mulligan
for(Player p : whoCanMulligan) {
if (p.getStartingHandSize() > p.getZone(ZoneType.Hand).size()) {
p.scry(1);
p.scry(1, null);
}
}
}

View File

@@ -24,6 +24,7 @@ import forge.game.event.GameEventPlayerPoisoned;
import forge.game.event.GameEventScry;
import forge.game.event.GameEventSpellAbilityCast;
import forge.game.event.GameEventSpellResolved;
import forge.game.event.GameEventSurveil;
import forge.game.event.GameEventTurnBegan;
import forge.game.event.GameEventTurnPhase;
import forge.game.event.IGameEventVisitor;
@@ -65,7 +66,24 @@ public class GameLogFormatter extends IGameEventVisitor.Base<GameLogEntry> {
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome);
}
@Override
public GameLogEntry visit(GameEventSurveil ev) {
String scryOutcome = "";
String toLibrary = Lang.nounWithAmount(ev.toLibrary, "card") + " to the top of the library";
String toGraveyard = Lang.nounWithAmount(ev.toGraveyard, "card") + " to the graveyard";
if (ev.toLibrary > 0 && ev.toGraveyard > 0) {
scryOutcome = ev.player.toString() + " surveil " + toLibrary + " and " + toGraveyard;
} else if (ev.toGraveyard == 0) {
scryOutcome = ev.player.toString() + " surveil " + toLibrary;
} else {
scryOutcome = ev.player.toString() + " surveil " + toGraveyard;
}
return new GameLogEntry(GameLogEntryType.STACK_RESOLVE, scryOutcome);
}
@Override
public GameLogEntry visit(GameEventSpellResolved ev) {
String messageForLog = ev.hasFizzled ? ev.spell.getHostCard().getName() + " ability fizzles." : ev.spell.getStackDescription();

View File

@@ -144,6 +144,7 @@ public enum ApiType {
SkipTurn (SkipTurnEffect.class),
StoreSVar (StoreSVarEffect.class),
StoreMap (StoreMapEffect.class),
Surveil (SurveilEffect.class),
Tap (TapEffect.class),
TapAll (TapAllEffect.class),
TapOrUntap (TapOrUntapEffect.class),

View File

@@ -45,7 +45,7 @@ public class ScryEffect extends SpellAbilityEffect {
continue;
}
p.scry(num);
p.scry(num, sa);
}
}
}

View File

@@ -0,0 +1,46 @@
package forge.game.ability.effects;
import forge.game.ability.AbilityUtils;
import forge.game.ability.SpellAbilityEffect;
import forge.game.player.Player;
import forge.game.spellability.SpellAbility;
import forge.util.Lang;
public class SurveilEffect extends SpellAbilityEffect {
@Override
protected String getStackDescription(SpellAbility sa) {
final StringBuilder sb = new StringBuilder();
sb.append(Lang.joinHomogenous(getTargetPlayers(sa)));
int num = 1;
if (sa.hasParam("Amount")) {
num = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa);
}
sb.append(" surveil (").append(num).append(").");
return sb.toString();
}
@Override
public void resolve(SpellAbility sa) {
int num = 1;
if (sa.hasParam("Amount")) {
num = AbilityUtils.calculateAmount(sa.getHostCard(), sa.getParam("Amount"), sa);
}
boolean isOptional = sa.hasParam("Optional");
for (final Player p : getTargetPlayers(sa)) {
if (!sa.usesTargeting() || p.canBeTargetedBy(sa)) {
if (isOptional && !p.getController().confirmAction(sa, null, "Do you want to surveil?")) {
continue;
}
p.surveil(num, sa);
}
}
}
}

View File

@@ -0,0 +1,21 @@
package forge.game.event;
import forge.game.player.Player;
public class GameEventSurveil extends GameEvent {
public final Player player;
public final int toLibrary, toGraveyard;
public GameEventSurveil(Player player, int toLibrary, int toGraveyard) {
this.player = player;
this.toLibrary = toLibrary;
this.toGraveyard = toGraveyard;
}
@Override
public <T> T visit(IGameEventVisitor<T> visitor) {
return visitor.visit(this);
}
}

View File

@@ -42,6 +42,7 @@ public interface IGameEventVisitor<T> {
T visit(GameEventSpellAbilityCast gameEventSpellAbilityCast);
T visit(GameEventSpellResolved event);
T visit(GameEventSpellRemovedFromStack event);
T visit(GameEventSurveil event);
T visit(GameEventTokenCreated event);
T visit(GameEventTurnBegan gameEventTurnBegan);
T visit(GameEventTurnEnded event);
@@ -87,6 +88,7 @@ public interface IGameEventVisitor<T> {
public T visit(GameEventSpellResolved event) { return null; }
public T visit(GameEventSpellAbilityCast event) { return null; }
public T visit(GameEventSpellRemovedFromStack event) { return null; }
public T visit(GameEventSurveil event) { return null; }
public T visit(GameEventTokenCreated event) { return null; }
public T visit(GameEventTurnBegan event) { return null; }
public T visit(GameEventTurnEnded event) { return null; }

View File

@@ -1248,15 +1248,11 @@ public class Player extends GameEntity implements Comparable<Player> {
return drawCards(1);
}
public void scry(final int numScry) {
final CardCollection topN = new CardCollection();
final PlayerZone library = getZone(ZoneType.Library);
final int actualNumScry = Math.min(numScry, library.size());
public void scry(final int numScry, SpellAbility cause) {
final CardCollection topN = new CardCollection(this.getCardsIn(ZoneType.Library, numScry));
if (actualNumScry == 0) { return; }
for (int i = 0; i < actualNumScry; i++) {
topN.add(library.get(i));
if (topN.isEmpty()) {
return;
}
final ImmutablePair<CardCollection, CardCollection> lists = getController().arrangeForScry(topN);
@@ -1268,7 +1264,7 @@ public class Player extends GameEntity implements Comparable<Player> {
if (toBottom != null) {
for(Card c : toBottom) {
getGame().getAction().moveToBottomOfLibrary(c, null, null);
getGame().getAction().moveToBottomOfLibrary(c, cause, null);
numToBottom++;
}
}
@@ -1276,7 +1272,7 @@ public class Player extends GameEntity implements Comparable<Player> {
if (toTop != null) {
Collections.reverse(toTop); // the last card in list will become topmost in library, have to revert thus.
for(Card c : toTop) {
getGame().getAction().moveToLibrary(c, null, null);
getGame().getAction().moveToLibrary(c, cause, null);
numToTop++;
}
}
@@ -1288,6 +1284,42 @@ public class Player extends GameEntity implements Comparable<Player> {
getGame().getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
}
public void surveil(final int num, SpellAbility cause) {
final CardCollection topN = new CardCollection(this.getCardsIn(ZoneType.Library, num));
if (topN.isEmpty()) {
return;
}
final ImmutablePair<CardCollection, CardCollection> lists = getController().arrangeForSurveil(topN);
final CardCollection toTop = lists.getLeft();
final CardCollection toGrave = lists.getRight();
int numToGrave = 0;
int numToTop = 0;
if (toGrave != null) {
for(Card c : toGrave) {
getGame().getAction().moveToGraveyard(c, cause, null);
numToGrave++;
}
}
if (toTop != null) {
Collections.reverse(toTop); // the last card in list will become topmost in library, have to revert thus.
for(Card c : toTop) {
getGame().getAction().moveToLibrary(c, cause, null);
numToTop++;
}
}
getGame().fireEvent(new GameEventSurveil(this, numToTop, numToGrave));
//final Map<String, Object> runParams = Maps.newHashMap();
//runParams.put("Player", this);
//getGame().getTriggerHandler().runTrigger(TriggerType.Scry, runParams, false);
}
public boolean canMulligan() {
return !getZone(ZoneType.Hand).isEmpty();
}

View File

@@ -140,6 +140,8 @@ public abstract class PlayerController {
/** Shows message to player to reveal chosen cardName, creatureType, number etc. AI must analyze API to understand what that is */
public abstract void notifyOfValue(SpellAbility saSource, GameObject realtedTarget, String value);
public abstract ImmutablePair<CardCollection, CardCollection> arrangeForScry(CardCollection topN);
public abstract ImmutablePair<CardCollection, CardCollection> arrangeForSurveil(CardCollection topN);
public abstract boolean willPutCardOnTop(Card c);
public final CardCollectionView orderMoveToZoneList(CardCollectionView cards, ZoneType destinationZone) {
return orderMoveToZoneList(cards, destinationZone, null);

View File

@@ -246,6 +246,11 @@ public class PlayerControllerForTests extends PlayerController {
return ImmutablePair.of(topN, null);
}
@Override
public ImmutablePair<CardCollection, CardCollection> arrangeForSurveil(CardCollection topN) {
return ImmutablePair.of(topN, null);
}
@Override
public boolean willPutCardOnTop(Card c) {
return false;

View File

@@ -1,6 +1,6 @@
Name:Deadly Visit
ManaCost:3 B B
Types:Sorcery
A:SP$ Destroy | Cost$ 3 B B | ValidTgts$ Creature | TgtPrompt$ Select target creature | Subability$ DBSurveil | SpellDescription$ Destroy target creature.
SVar:DBSurveil:DB$ Surveil | SurveilNum$ 2
A:SP$ Destroy | Cost$ 3 B B | ValidTgts$ Creature | TgtPrompt$ Select target creature | Subability$ DBSurveil | SpellDescription$ Destroy target creature. Surveil 2 (Look at the top two cards of your library, then put any number of them into your graveyard and the rest on top of your library in any order.)
SVar:DBSurveil:DB$ Surveil | Amount$ 2
Oracle:Destroy target creature.\nSurveil 2. (Look at the top two cards of your library, then put any number of them into your graveyard and the rest on top of your library in any order.)

View File

@@ -1,6 +1,6 @@
Name:Sinister Sabotage
ManaCost:1 U U
Types:Instant
A:SP$ Counter | Cost$ 1 U U | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | Subability$ DBSurveil | SpellDescription$ Counter target spell.1 (Look at the top card of your library. You may put that card into your graveyard.)
SVar:DBSurveil:DB$ Surveil | SurveilNum$ 1
A:SP$ Counter | Cost$ 1 U U | TargetType$ Spell | TgtPrompt$ Select target spell | ValidTgts$ Card | Subability$ DBSurveil | SpellDescription$ Counter target spell. Surveil 1 (Look at the top card of your library. You may put that card into your graveyard.)
SVar:DBSurveil:DB$ Surveil | Amount$ 1
Oracle:Counter target spell.\nSurveil 1. (Look at the top card of your library. You may put that card into your graveyard.)

View File

@@ -1,6 +1,6 @@
Name:Thought Erasure
ManaCost:U B
Types:Sorcery
A:SP$ Discard | Cost$ U B | ValidTgts$ Opponent | DiscardValid$ Card.nonLand | NumCards$ 1 | Mode$ RevealYouChoose | Subability$ DBSurveil | SpellDescription$ Target opponent reveals their hand. You choose a nonland card from it. That player discards that card.
SVar:DBSurveil:DB$ Surveil | SurveilNum$ 1
A:SP$ Discard | Cost$ U B | ValidTgts$ Opponent | DiscardValid$ Card.nonLand | NumCards$ 1 | Mode$ RevealYouChoose | Subability$ DBSurveil | SpellDescription$ Target opponent reveals their hand. You choose a nonland card from it. That player discards that card. Surveil 1 (Look at the top card of your library. You may put that card into your graveyard.)
SVar:DBSurveil:DB$ Surveil | Amount$ 1
Oracle:Target opponent reveals their hand. You choose a nonland card from it. That player discards that card.\nSurveil 1. (Look at the top card of your library. You may put it into your graveyard.)

View File

@@ -1,6 +1,6 @@
Name:Unexplained Disappearance
ManaCost:1 U
Types:Instant
A:SP$ ChangeZone | Cost$ 1 U | ValidTgts$ Creature | TgtPrompt$ Select target creature | Origin$ Battlefield | Destination$ Hand | Subability$ DBSurveil | SpellDescription$ Return target creature to its owner's hand.
SVar:DBSurveil:DB$ Surveil | SurveilNum$ 1
A:SP$ ChangeZone | Cost$ 1 U | ValidTgts$ Creature | TgtPrompt$ Select target creature | Origin$ Battlefield | Destination$ Hand | Subability$ DBSurveil | SpellDescription$ Return target creature to its owner's hand. Surveil 1 (Look at the top card of your library. You may put that card into your graveyard.)
SVar:DBSurveil:DB$ Surveil | Amount$ 1
Oracle:Return target creature to its owner's hand.\nSurveil 1. (Look at the top card of your library. You may put that card into your graveyard.)

View File

@@ -769,6 +769,43 @@ public class PlayerControllerHuman extends PlayerController implements IGameCont
return ImmutablePair.of(toTop, toBottom);
}
@Override
public ImmutablePair<CardCollection, CardCollection> arrangeForSurveil(final CardCollection topN) {
CardCollection toGrave = null;
CardCollection toTop = null;
tempShowCards(topN);
if (topN.size() == 1) {
final Card c = topN.getFirst();
final CardView view = CardView.get(c);
tempShowCard(c);
getGui().setCard(view);
boolean result = false;
result = InputConfirm.confirm(this, view, TextUtil.concatNoSpace("Put ", view.toString(), " on the top of library or graveyard?"),
true, ImmutableList.of("Library", "Graveyard"));
if (result) {
toTop = topN;
} else {
toGrave = topN;
}
} else {
toGrave = game.getCardList(getGui().many("Select cards to be put into the graveyard",
"Cards to put in the graveyard", -1, CardView.getCollection(topN), null));
topN.removeAll((Collection<?>) toGrave);
if (topN.isEmpty()) {
toTop = null;
} else if (topN.size() == 1) {
toTop = topN;
} else {
toTop = game.getCardList(getGui().order("Arrange cards to be put on top of your library",
"Top of Library", CardView.getCollection(topN), null));
}
}
endTempShowCards();
return ImmutablePair.of(toTop, toGrave);
}
@Override
public boolean willPutCardOnTop(final Card c) {
final CardView view = CardView.get(c);