forked from External/mage
AI: reworked blockers selections:
* fixed game freezes for no-possible block configurations like Menace (#13290); * fixed computer cheating to ignore block requirements like Menace (now AI will choose all required blockers instead 1); * improved computer logic for blockers selection (try to sacrifice a creature instead game loose, simple use cases only); * added freeze protection for bad or unsupported attacker-block configuration; * refactor: deleted outdated AI code;
This commit is contained in:
parent
a99871988d
commit
92b7ed8efc
12 changed files with 495 additions and 202 deletions
|
|
@ -174,7 +174,7 @@ public interface Game extends MageItem, Serializable, Copyable<Game> {
|
|||
*
|
||||
* Warning, it will return leaved players until end of turn. For dialogs and one shot effects use excludeLeavedPlayers
|
||||
*/
|
||||
// TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers
|
||||
// TODO: check usage of getOpponents in cards and replace with correct call of excludeLeavedPlayers, see #13289
|
||||
default Set<UUID> getOpponents(UUID playerId) {
|
||||
return getOpponents(playerId, false);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3174,7 +3174,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.INFO, message, this);
|
||||
}
|
||||
|
||||
|
|
@ -3183,7 +3183,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.STATUS, message, withTime, withTurnInfo, this);
|
||||
}
|
||||
|
||||
|
|
@ -3192,7 +3192,7 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.UPDATE, null, this);
|
||||
getState().clearLookedAt();
|
||||
getState().clearRevealed();
|
||||
|
|
@ -3203,23 +3203,23 @@ public abstract class GameImpl implements Game {
|
|||
if (simulation) {
|
||||
return;
|
||||
}
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.END_GAME_INFO, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void fireErrorEvent(String message, Exception ex) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.ERROR, message, ex, this);
|
||||
}
|
||||
|
||||
private void makeSureCalledOutsideLayersEffects() {
|
||||
private void makeSureCalledOutsideLayerEffects() {
|
||||
// very slow, enable/comment it for debug or load/stability tests only
|
||||
// TODO: enable check and remove/rework all wrong usages
|
||||
if (true) return;
|
||||
Arrays.stream(Thread.currentThread().getStackTrace()).forEach(e -> {
|
||||
if (e.toString().contains("GameState.applyEffects")) {
|
||||
throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?");
|
||||
throw new IllegalStateException("Wrong code usage: client side events can't be called from layers effects (wrong informPlayers usage?)");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -3890,7 +3890,7 @@ public abstract class GameImpl implements Game {
|
|||
@Override
|
||||
public void initTimer(UUID playerId) {
|
||||
if (priorityTime > 0) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.INIT_TIMER, playerId, null, this);
|
||||
}
|
||||
}
|
||||
|
|
@ -3898,7 +3898,7 @@ public abstract class GameImpl implements Game {
|
|||
@Override
|
||||
public void resumeTimer(UUID playerId) {
|
||||
if (priorityTime > 0) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.RESUME_TIMER, playerId, null, this);
|
||||
}
|
||||
}
|
||||
|
|
@ -3906,7 +3906,7 @@ public abstract class GameImpl implements Game {
|
|||
@Override
|
||||
public void pauseTimer(UUID playerId) {
|
||||
if (priorityTime > 0) {
|
||||
makeSureCalledOutsideLayersEffects();
|
||||
makeSureCalledOutsideLayerEffects();
|
||||
tableEventSource.fireTableEvent(EventType.PAUSE_TIMER, playerId, null, this);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -670,9 +670,25 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
}
|
||||
|
||||
// choosing until good block configuration
|
||||
int aiTries = 0;
|
||||
while (true) {
|
||||
aiTries++;
|
||||
|
||||
if (controller.isComputer() && aiTries > 20) {
|
||||
// TODO: AI must use real attacker/blocker configuration with all possible combination
|
||||
// (current human like logic will fail sometime, e.g. with menace and big/low creatures)
|
||||
// real game: send warning
|
||||
// test: fast fail
|
||||
game.informPlayers(controller.getLogName() + ": WARNING - AI can't find good blocker combination and will skip it - report your battlefield to github - " + game.getCombat());
|
||||
if (controller.isTestsMode()) {
|
||||
// how-to fix: AI code must support failed abilities or use cases
|
||||
throw new IllegalArgumentException("AI can't find good blocker combination");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// declare normal blockers
|
||||
// TODO: need reseach - is it possible to concede on bad blocker configuration (e.g. user can't continue)
|
||||
// TODO: need research - is it possible to concede on bad blocker configuration (e.g. user can't continue)
|
||||
controller.selectBlockers(source, game, defenderId);
|
||||
if (game.isPaused() || game.checkIfGameIsOver() || game.executingRollback()) {
|
||||
return;
|
||||
|
|
@ -776,18 +792,16 @@ public class Combat implements Serializable, Copyable<Combat> {
|
|||
/**
|
||||
* Check the block restrictions
|
||||
*
|
||||
* @param player
|
||||
* @param game
|
||||
* @return false - if block restrictions were not complied
|
||||
*/
|
||||
public boolean checkBlockRestrictions(Player player, Game game) {
|
||||
public boolean checkBlockRestrictions(Player defender, Game game) {
|
||||
int count = 0;
|
||||
boolean blockWasLegal = true;
|
||||
for (CombatGroup group : groups) {
|
||||
count += group.getBlockers().size();
|
||||
}
|
||||
for (CombatGroup group : groups) {
|
||||
blockWasLegal &= group.checkBlockRestrictions(game, count);
|
||||
blockWasLegal &= group.checkBlockRestrictions(game, defender, count);
|
||||
}
|
||||
return blockWasLegal;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -773,11 +773,27 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
}
|
||||
}
|
||||
|
||||
public boolean checkBlockRestrictions(Game game, int blockersCount) {
|
||||
public boolean checkBlockRestrictions(Game game, Player defender, int blockersCount) {
|
||||
boolean blockWasLegal = true;
|
||||
if (attackers.isEmpty()) {
|
||||
return blockWasLegal;
|
||||
}
|
||||
|
||||
// collect possible blockers
|
||||
Map<UUID, Set<UUID>> possibleBlockers = new HashMap<>();
|
||||
for (UUID attackerId : attackers) {
|
||||
Permanent attacker = game.getPermanent(attackerId);
|
||||
Set<UUID> goodBlockers = new HashSet<>();
|
||||
for (Permanent blocker : game.getBattlefield().getActivePermanents(StaticFilters.FILTER_PERMANENT_CREATURES_CONTROLLED, defender.getId(), game)) {
|
||||
if (blocker.canBlock(attackerId, game)) {
|
||||
goodBlockers.add(blocker.getId());
|
||||
}
|
||||
}
|
||||
possibleBlockers.put(attacker.getId(), goodBlockers);
|
||||
}
|
||||
|
||||
// effects: can't block alone
|
||||
// too much blockers
|
||||
if (blockersCount == 1) {
|
||||
List<UUID> toBeRemoved = new ArrayList<>();
|
||||
for (UUID blockerId : getBlockers()) {
|
||||
|
|
@ -802,19 +818,27 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
for (UUID uuid : attackers) {
|
||||
Permanent attacker = game.getPermanent(uuid);
|
||||
if (attacker != null && this.blocked) {
|
||||
// Check if there are enough blockers to have a legal block
|
||||
// effects: can't be blocked except by xxx or more creatures
|
||||
// too few blockers
|
||||
if (attacker.getMinBlockedBy() > 1 && !blockers.isEmpty() && blockers.size() < attacker.getMinBlockedBy()) {
|
||||
for (UUID blockerId : new ArrayList<>(blockers)) {
|
||||
game.getCombat().removeBlocker(blockerId, game);
|
||||
game.getCombat().removeBlocker(blockerId, game); // !
|
||||
}
|
||||
blockers.clear();
|
||||
blockerOrder.clear();
|
||||
if (!game.isSimulation()) {
|
||||
game.informPlayers(attacker.getLogName() + " can't be blocked except by " + attacker.getMinBlockedBy() + " or more creatures. Blockers discarded.");
|
||||
}
|
||||
blockWasLegal = false;
|
||||
|
||||
// if there aren't any possible blocker configuration then it's legal due mtg rules
|
||||
// warning, it's affect AI related logic like other block auto-fixes does, see https://github.com/magefree/mage/pull/13182
|
||||
if (attacker.getMinBlockedBy() <= possibleBlockers.getOrDefault(attacker.getId(), Collections.emptySet()).size()) {
|
||||
blockWasLegal = false;
|
||||
}
|
||||
}
|
||||
// Check if there are too many blockers (maxBlockedBy = 0 means no restrictions)
|
||||
|
||||
// effects: can't be blocked by more than xxx creature
|
||||
// too much blockers
|
||||
if (attacker.getMaxBlockedBy() > 0 && attacker.getMaxBlockedBy() < blockers.size()) {
|
||||
for (UUID blockerId : new ArrayList<>(blockers)) {
|
||||
game.getCombat().removeBlocker(blockerId, game);
|
||||
|
|
@ -827,6 +851,7 @@ public class CombatGroup implements Serializable, Copyable<CombatGroup> {
|
|||
.append(attacker.getMaxBlockedBy() == 1 ? " creature." : " creatures.")
|
||||
.append(" Blockers discarded.").toString());
|
||||
}
|
||||
|
||||
blockWasLegal = false;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue