other: reworked target selection: (#13638)

- WIP: AI and multi targets, human and X=0 use cases, human and impossible targets use cases;
- improved stability and shared logic (related to #13606, #11134, #11666, continue from a53eb66b58, close #13617, close #13613);
- improved test logs and debug info to show more target info on errors;
- improved test framework to support multiple addTarget calls;
- improved test framework to find bad commands order for targets (related to #11666);
- fixed game freezes on auto-choice usages with disconnected or under control players (related to #11285);
- gui, game: fixed that player doesn't mark avatar as selected/green in "up to" targeting;
- gui, game: fixed small font in some popup messages on big screens (related to #969);
- gui, game: added min targets info for target selection dialog;
- for devs: added new cheat option to call and test any game dialog (define own dialogs, targets, etc in HumanDialogsTester);
- for devs: now tests require complete an any or up to target selection by addTarget + TestPlayer.TARGET_SKIP or setChoice + TestPlayer.CHOICE_SKIP (if not all max/possible targets used);
- for devs: added detail targets info for activate/trigger/cast, can be useful to debug unit tests, auto-choose or AI (see DebugUtil.GAME_SHOW_CHOOSE_TARGET_LOGS)
This commit is contained in:
Oleg Agafonov 2025-05-16 13:55:54 +04:00 committed by GitHub
parent 80d62727e1
commit 133e4fe425
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
84 changed files with 2737 additions and 743 deletions

View file

@ -0,0 +1,201 @@
package mage.utils.testers;
import mage.abilities.Ability;
import mage.choices.Choice;
import mage.choices.ChoiceHintType;
import mage.choices.ChoiceImpl;
import mage.constants.Outcome;
import mage.game.Game;
import mage.players.Player;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* Part of testable game dialogs
* <p>
* Helper class to create additional options in cheat menu - allow to test any game dialogs with any settings
* Allow to call game dialogs from you or from your opponent, e.g. from AI player
* <p>
* All existing human's choose dialogs (search by waitForResponse):
* <p>
* Support of single dialogs (can be called inside effects):
* [x] choose(target)
* [x] choose(cards)
* [x] choose(choice)
* [x] chooseTarget(target)
* [x] chooseTarget(cards)
* [x] chooseTargetAmount
* [x] chooseUse
* [x] choosePile
* [ ] announceXMana // TODO: implement
* [ ] announceXCost // TODO: implement
* [ ] getAmount // TODO: implement
* [ ] getMultiAmountWithIndividualConstraints // TODO: implement
* <p>
* Support of priority dialogs (can be called by game engine, some can be implemented in theory):
* --- priority
* --- playManaHandling
* --- activateSpecialAction
* --- activateAbility
* --- chooseAbilityForCast
* --- chooseLandOrSpellAbility
* --- chooseMode
* --- chooseMulligan
* --- chooseReplacementEffect
* --- chooseTriggeredAbility
* --- selectAttackers
* --- selectBlockers
* --- selectCombatGroup (part of selectBlockers)
* <p>
* Support of outdated dialogs (not used anymore)
* --- announceRepetitions (part of removed macro feature)
*
* @author JayDi85
*/
public class TestableDialogsRunner {
private final List<TestableDialog> dialogs = new ArrayList<>();
static final int LAST_SELECTED_GROUP_ID = 997;
static final int LAST_SELECTED_DIALOG_ID = 998;
// for better UX - save last selected options, so can return to it later
// it's ok to have it global, cause test mode for local and single user environment
static String lastSelectedGroup = null;
static TestableDialog lastSelectedDialog = null;
public TestableDialogsRunner() {
ChooseTargetTestableDialog.register(this);
ChooseCardsTestableDialog.register(this);
ChooseUseTestableDialog.register(this);
ChooseChoiceTestableDialog.register(this);
ChoosePileTestableDialog.register(this);
ChooseAmountTestableDialog.register(this);
}
void registerDialog(TestableDialog dialog) {
this.dialogs.add(dialog);
}
public void selectAndShowTestableDialog(Player player, Ability source, Game game, Player opponent) {
// select group or fast links
List<String> groups = this.dialogs.stream()
.map(TestableDialog::getGroup)
.distinct()
.sorted()
.collect(Collectors.toList());
Choice choice = prepareSelectGroupChoice(groups);
player.choose(Outcome.Benefit, choice, game);
String needGroup = null;
TestableDialog needDialog = null;
if (choice.getChoiceKey() != null) {
int needIndex = Integer.parseInt(choice.getChoiceKey());
if (needIndex == LAST_SELECTED_GROUP_ID && lastSelectedGroup != null) {
// fast link to group
needGroup = lastSelectedGroup;
} else if (needIndex == LAST_SELECTED_DIALOG_ID && lastSelectedDialog != null) {
// fast link to dialog
needGroup = lastSelectedDialog.getGroup();
needDialog = lastSelectedDialog;
} else if (needIndex < groups.size()) {
// group
needGroup = groups.get(needIndex);
}
}
if (needGroup == null) {
return;
}
// select dialog
if (needDialog == null) {
choice = prepareSelectDialogChoice(needGroup);
player.choose(Outcome.Benefit, choice, game);
if (choice.getChoiceKey() != null) {
int needIndex = Integer.parseInt(choice.getChoiceKey());
if (needIndex < this.dialogs.size()) {
needDialog = this.dialogs.get(needIndex);
}
}
}
if (needDialog == null) {
return;
}
// all fine, can show it and finish
lastSelectedGroup = needGroup;
lastSelectedDialog = needDialog;
List<String> resInfo = needDialog.showDialog(player, source, game, opponent);
needDialog.showResult(player, game, String.join("<br>", resInfo));
}
private Choice prepareSelectGroupChoice(List<String> groups) {
// try to choose group or fast links
Choice choice = new ChoiceImpl(false);
choice.setMessage("Choose dialogs group to run");
// main groups
int recNumber = 0;
for (int i = 0; i < groups.size(); i++) {
recNumber++;
String group = groups.get(i);
choice.withItem(
String.valueOf(i),
String.format("%02d. %s", recNumber, group),
recNumber,
ChoiceHintType.TEXT,
String.join("<br>", group)
);
}
// fast link to last group
String lastGroupInfo = String.format(" -> last group: %s", lastSelectedGroup == null ? "not used" : lastSelectedGroup);
choice.withItem(
String.valueOf(LAST_SELECTED_GROUP_ID),
lastGroupInfo,
-2,
ChoiceHintType.TEXT,
lastGroupInfo
);
// fast link to last dialog
String lastDialogName = (lastSelectedDialog == null ? "not used" : String.format("%s - %s",
lastSelectedDialog.getName(), lastSelectedDialog.getDescription()));
String lastDialogInfo = String.format(" -> last dialog: %s", lastDialogName);
choice.withItem(
String.valueOf(LAST_SELECTED_DIALOG_ID),
lastDialogInfo,
-1,
ChoiceHintType.TEXT,
lastDialogInfo
);
return choice;
}
private Choice prepareSelectDialogChoice(String needGroup) {
Choice choice = new ChoiceImpl(false);
choice.setMessage("Choose game dialog to run from " + needGroup);
int recNumber = 0;
for (int i = 0; i < this.dialogs.size(); i++) {
TestableDialog dialog = this.dialogs.get(i);
if (!dialog.getGroup().equals(needGroup)) {
continue;
}
recNumber++;
String info = String.format("%s - %s - %s", dialog.getGroup(), dialog.getName(), dialog.getDescription());
choice.withItem(
String.valueOf(i),
String.format("%02d. %s", recNumber, info),
recNumber,
ChoiceHintType.TEXT,
String.join("<br>", info)
);
}
return choice;
}
}