Turn under control reworked:

- game: added support when a human is take control over a computer player (related to #12878);
 - game: fixed game freezes while controlling player leaves/disconnect on active priority/choose of another player;
This commit is contained in:
Oleg Agafonov 2025-01-10 20:18:00 +04:00
parent 49b90820e0
commit 0505f5159e
12 changed files with 534 additions and 32 deletions

View file

@ -0,0 +1,401 @@
package mage.player.ai;
import mage.MageObject;
import mage.abilities.*;
import mage.abilities.costs.VariableCost;
import mage.abilities.costs.mana.ManaCost;
import mage.cards.Card;
import mage.cards.Cards;
import mage.cards.decks.Deck;
import mage.choices.Choice;
import mage.constants.ManaType;
import mage.constants.MultiAmountType;
import mage.constants.Outcome;
import mage.constants.RangeOfInfluence;
import mage.game.Game;
import mage.game.combat.CombatGroup;
import mage.game.draft.Draft;
import mage.game.match.Match;
import mage.game.permanent.Permanent;
import mage.game.tournament.Tournament;
import mage.players.Player;
import mage.target.Target;
import mage.target.TargetAmount;
import mage.target.TargetCard;
import mage.util.MultiAmountMessage;
import org.apache.log4j.Logger;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
import java.util.UUID;
/**
* AI player that can be taken under control by another player (AI or human).
* <p>
* Under control logic on choose dialog (under human):
* - create fake human player and assign it to real human data transfer object (for income answers);
* - call choose dialog from fake human (e.g. send choose data to real player);
* - game will process all sending and answering logic as "human under human" logic;
* - return choose dialog result without AI code processing;
*
* @author JayDi85
*/
public class ComputerPlayerControllableProxy extends ComputerPlayer7 {
private static final Logger logger = Logger.getLogger(ComputerPlayerControllableProxy.class);
Player lastControllingPlayer = null;
public ComputerPlayerControllableProxy(String name, RangeOfInfluence range, int skill) {
super(name, range, skill);
}
public ComputerPlayerControllableProxy(final ComputerPlayerControllableProxy player) {
super(player);
this.lastControllingPlayer = player;
}
@Override
public ComputerPlayerControllableProxy copy() {
return new ComputerPlayerControllableProxy(this);
}
private boolean isUnderMe(Game game) {
return game.isSimulation() || this.isGameUnderControl();
}
private Player getControllingPlayer(Game game) {
Player player = game.getPlayer(this.getTurnControlledBy());
this.lastControllingPlayer = player.prepareControllableProxy(this);
return this.lastControllingPlayer;
}
@Override
public void setResponseString(String responseString) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseString(responseString);
}
}
@Override
public void setResponseManaType(UUID manaTypePlayerId, ManaType responseManaType) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseManaType(manaTypePlayerId, responseManaType);
}
}
@Override
public void setResponseUUID(UUID responseUUID) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseUUID(responseUUID);
}
}
@Override
public void setResponseBoolean(Boolean responseBoolean) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseBoolean(responseBoolean);
}
}
@Override
public void setResponseInteger(Integer responseInteger) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.setResponseInteger(responseInteger);
}
}
@Override
public void signalPlayerCheat() {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.signalPlayerCheat();
}
}
@Override
public void signalPlayerConcede(boolean stopCurrentChooseDialog) {
if (this.lastControllingPlayer != null) {
this.lastControllingPlayer.signalPlayerConcede(stopCurrentChooseDialog);
}
}
@Override
public boolean priority(Game game) {
if (isUnderMe(game)) {
return super.priority(game);
} else {
Player player = getControllingPlayer(game);
try {
return player.priority(game);
} finally {
this.passed = player.isPassed(); // TODO: wtf, no needs?
}
}
}
@Override
public boolean chooseMulligan(Game game) {
if (isUnderMe(game)) {
return super.chooseMulligan(game);
} else {
return getControllingPlayer(game).chooseMulligan(game);
}
}
@Override
public boolean chooseUse(Outcome outcome, String message, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseUse(outcome, message, source, game);
} else {
return getControllingPlayer(game).chooseUse(outcome, message, source, game);
}
}
@Override
public boolean chooseUse(Outcome outcome, String message, String secondMessage, String trueText, String falseText, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseUse(outcome, message, secondMessage, trueText, falseText, source, game);
} else {
return getControllingPlayer(game).chooseUse(outcome, message, secondMessage, trueText, falseText, source, game);
}
}
@Override
public int chooseReplacementEffect(Map<String, String> effectsMap, Map<String, MageObject> objectsMap, Game game) {
if (isUnderMe(game)) {
return super.chooseReplacementEffect(effectsMap, objectsMap, game);
} else {
return getControllingPlayer(game).chooseReplacementEffect(effectsMap, objectsMap, game);
}
}
@Override
public boolean choose(Outcome outcome, Choice choice, Game game) {
if (isUnderMe(game)) {
return super.choose(outcome, choice, game);
} else {
return getControllingPlayer(game).choose(outcome, choice, game);
}
}
@Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.choose(outcome, target, source, game);
} else {
return getControllingPlayer(game).choose(outcome, target, source, game);
}
}
@Override
public boolean choose(Outcome outcome, Target target, Ability source, Game game, Map<String, Serializable> options) {
if (isUnderMe(game)) {
return super.choose(outcome, target, source, game, options);
} else {
return getControllingPlayer(game).choose(outcome, target, source, game, options);
}
}
@Override
public boolean chooseTarget(Outcome outcome, Target target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseTarget(outcome, target, source, game);
} else {
return getControllingPlayer(game).chooseTarget(outcome, target, source, game);
}
}
@Override
public boolean choose(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.choose(outcome, cards, target, source, game);
} else {
return getControllingPlayer(game).choose(outcome, cards, target, source, game);
}
}
@Override
public boolean chooseTarget(Outcome outcome, Cards cards, TargetCard target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseTarget(outcome, cards, target, source, game);
} else {
return getControllingPlayer(game).chooseTarget(outcome, cards, target, source, game);
}
}
@Override
public boolean chooseTargetAmount(Outcome outcome, TargetAmount target, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseTargetAmount(outcome, target, source, game);
} else {
return getControllingPlayer(game).chooseTargetAmount(outcome, target, source, game);
}
}
@Override
public TriggeredAbility chooseTriggeredAbility(java.util.List<TriggeredAbility> abilities, Game game) {
if (isUnderMe(game)) {
return super.chooseTriggeredAbility(abilities, game);
} else {
return getControllingPlayer(game).chooseTriggeredAbility(abilities, game);
}
}
@Override
public boolean playMana(Ability abilityToCast, ManaCost unpaid, String promptText, Game game) {
if (isUnderMe(game)) {
return super.playMana(abilityToCast, unpaid, promptText, game);
} else {
return getControllingPlayer(game).playMana(abilityToCast, unpaid, promptText, game);
}
}
@Override
public int announceXMana(int min, int max, String message, Game game, Ability ability) {
if (isUnderMe(game)) {
return super.announceXMana(min, max, message, game, ability);
} else {
return getControllingPlayer(game).announceXMana(min, max, message, game, ability);
}
}
@Override
public int announceXCost(int min, int max, String message, Game game, Ability ability, VariableCost variableCost) {
if (isUnderMe(game)) {
return super.announceXCost(min, max, message, game, ability, variableCost);
} else {
return getControllingPlayer(game).announceXCost(min, max, message, game, ability, variableCost);
}
}
@Override
public void selectAttackers(Game game, UUID attackingPlayerId) {
if (isUnderMe(game)) {
super.selectAttackers(game, attackingPlayerId);
} else {
getControllingPlayer(game).selectAttackers(game, attackingPlayerId);
}
}
@Override
public void selectBlockers(Ability source, Game game, UUID defendingPlayerId) {
if (isUnderMe(game)) {
super.selectBlockers(source, game, defendingPlayerId);
} else {
getControllingPlayer(game).selectBlockers(source, game, defendingPlayerId);
}
}
@Override
public UUID chooseAttackerOrder(java.util.List<Permanent> attackers, Game game) {
if (isUnderMe(game)) {
return super.chooseAttackerOrder(attackers, game);
} else {
return getControllingPlayer(game).chooseAttackerOrder(attackers, game);
}
}
@Override
public UUID chooseBlockerOrder(java.util.List<Permanent> blockers, CombatGroup combatGroup, java.util.List<UUID> blockerOrder, Game game) {
if (isUnderMe(game)) {
return super.chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
} else {
return getControllingPlayer(game).chooseBlockerOrder(blockers, combatGroup, blockerOrder, game);
}
}
@Override
public int getAmount(int min, int max, String message, Game game) {
if (isUnderMe(game)) {
return super.getAmount(min, max, message, game);
} else {
return getControllingPlayer(game).getAmount(min, max, message, game);
}
}
@Override
public List<Integer> getMultiAmountWithIndividualConstraints(
Outcome outcome,
List<MultiAmountMessage> messages,
int totalMin,
int totalMax,
MultiAmountType type,
Game game
) {
if (isUnderMe(game)) {
return super.getMultiAmountWithIndividualConstraints(outcome, messages, totalMin, totalMax, type, game);
} else {
return getControllingPlayer(game).getMultiAmountWithIndividualConstraints(outcome, messages, totalMin, totalMax, type, game);
}
}
@Override
public void sideboard(Match match, Deck deck) {
super.sideboard(match, deck);
}
@Override
public void construct(Tournament tournament, Deck deck) {
super.construct(tournament, deck);
}
@Override
public void pickCard(java.util.List<Card> cards, Deck deck, Draft draft) {
super.pickCard(cards, deck, draft);
}
@Override
public boolean activateAbility(ActivatedAbility ability, Game game) {
// TODO: need research, see HumanPlayer's code
return super.activateAbility(ability, game);
}
@Override
public SpellAbility chooseAbilityForCast(Card card, Game game, boolean noMana) {
if (isUnderMe(game)) {
return super.chooseAbilityForCast(card, game, noMana);
} else {
return getControllingPlayer(game).chooseAbilityForCast(card, game, noMana);
}
}
@Override
public ActivatedAbility chooseLandOrSpellAbility(Card card, Game game, boolean noMana) {
if (isUnderMe(game)) {
return super.chooseLandOrSpellAbility(card, game, noMana);
} else {
return getControllingPlayer(game).chooseLandOrSpellAbility(card, game, noMana);
}
}
@Override
public Mode chooseMode(Modes modes, Ability source, Game game) {
if (isUnderMe(game)) {
return super.chooseMode(modes, source, game);
} else {
return getControllingPlayer(game).chooseMode(modes, source, game);
}
}
@Override
public boolean choosePile(Outcome outcome, String message, java.util.List<? extends Card> pile1, java.util.List<? extends Card> pile2, Game game) {
if (isUnderMe(game)) {
return super.choosePile(outcome, message, pile1, pile2, game);
} else {
return getControllingPlayer(game).choosePile(outcome, message, pile1, pile2, game);
}
}
@Override
public void abort() {
// TODO: need research, is it require real player call? Concede/leave/timeout works by default
super.abort();
}
@Override
public void skip() {
// TODO: see abort comments above
super.skip();
}
}

View file

@ -97,7 +97,7 @@ public class HumanPlayer extends PlayerImpl {
// * - GAME thread: on notify from response - check new answer value and process it (if it bad then repeat and wait the next one);
private transient Boolean responseOpenedForAnswer = false; // GAME thread waiting new answer
private transient long responseLastWaitingThreadId = 0;
private final transient PlayerResponse response = new PlayerResponse();
private final transient PlayerResponse response; // data receiver from a client side (must be shared for one player between multiple clients)
private final int RESPONSE_WAITING_TIME_SECS = 30; // waiting time before cancel current response
private final int RESPONSE_WAITING_CHECK_MS = 100; // timeout for open status check
@ -105,7 +105,7 @@ public class HumanPlayer extends PlayerImpl {
protected static FilterCreatureForCombat filterCreatureForCombat = new FilterCreatureForCombat();
protected static FilterAttackingCreature filterAttack = new FilterAttackingCreature();
protected static FilterBlockingCreature filterBlock = new FilterBlockingCreature();
protected final Choice replacementEffectChoice;
protected Choice replacementEffectChoice = null;
private static final Logger logger = Logger.getLogger(HumanPlayer.class);
protected HashSet<String> autoSelectReplacementEffects = new LinkedHashSet<>(); // must be sorted
@ -131,8 +131,12 @@ public class HumanPlayer extends PlayerImpl {
public HumanPlayer(String name, RangeOfInfluence range, int skill) {
super(name, range);
human = true;
this.human = true;
this.response = new PlayerResponse();
initReplacementDialog();
}
private void initReplacementDialog() {
replacementEffectChoice = new ChoiceImpl(true);
replacementEffectChoice.setMessage("Choose replacement effect to resolve first");
replacementEffectChoice.setSpecial(
@ -143,8 +147,20 @@ public class HumanPlayer extends PlayerImpl {
);
}
/**
* Make fake player from any other
*/
public HumanPlayer(final PlayerImpl sourcePlayer, final PlayerResponse sourceResponse) {
super(sourcePlayer);
this.human = true;
this.response = sourceResponse; // need for sync and wait user's response from a network
initReplacementDialog();
}
public HumanPlayer(final HumanPlayer player) {
super(player);
this.response = player.response;
this.replacementEffectChoice = player.replacementEffectChoice;
this.autoSelectReplacementEffects.addAll(player.autoSelectReplacementEffects);
this.currentlyUnpaidMana = player.currentlyUnpaidMana;
@ -2940,11 +2956,6 @@ public class HumanPlayer extends PlayerImpl {
return true;
}
@Override
public String getHistory() {
return "no available";
}
private boolean gameInCheckPlayableState(Game game) {
return gameInCheckPlayableState(game, false);
}
@ -2962,4 +2973,14 @@ public class HumanPlayer extends PlayerImpl {
}
return false;
}
@Override
public Player prepareControllableProxy(Player playerUnderControl) {
// make fake player, e.g. transform computer player to human player for choose dialogs under control
HumanPlayer fakePlayer = new HumanPlayer((PlayerImpl) playerUnderControl, this.response);
if (!fakePlayer.getTurnControlledBy().equals(this.getId())) {
throw new IllegalArgumentException("Wrong code usage: controllable proxy must be controlled by " + this.getName());
}
return fakePlayer;
}
}