Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break out main loop step for easier testing and AI dev: now with a feature flag! #5951

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions forge-core/src/main/java/forge/StaticData.java
Original file line number Diff line number Diff line change
Expand Up @@ -388,6 +388,10 @@ public IStorage<PrintSheet> getPrintSheets() {
return printSheets;
}

/**
* Get a database of all non-variant cards
* @return
*/
public CardDb getCommonCards() {
return commonCards;
}
Expand Down
8 changes: 8 additions & 0 deletions forge-core/src/main/java/forge/deck/Deck.java
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,14 @@ public Deck(final String name0) {
getOrCreate(DeckSection.Main);
}

/**
* Create a new deck from a cardpool
*/
public Deck(final String name0, CardPool main) {
super(name0);
getOrCreate(DeckSection.Main).add(main.toFlatList());
}

/**
* Copy constructor.
*
Expand Down
3 changes: 2 additions & 1 deletion forge-game/src/main/java/forge/game/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,9 @@ public class Game {

private final Zone stackZone = new Zone(ZoneType.Stack, this);

// While these are false here, they're really set by the Match/Preferences
public boolean EXPERIMENTAL_RESTORE_SNAPSHOT = false;
// While this is false here, its really set by the Match/Preferences
public boolean EXPERIMENTAL_LOOP = false;

// If this merges with LKI In the future, it will need to change forms
private GameSnapshot previousGameState = null;
Expand Down
166 changes: 166 additions & 0 deletions forge-game/src/main/java/forge/game/phase/PhaseHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -988,10 +988,176 @@ public final boolean skippedDeclareBlockers() {

private final static boolean DEBUG_PHASES = false;

public void setupFirstTurn(Player goesFirst, Runnable startGameHook) {
if (phase != null) {
throw new IllegalStateException("Turns already started, call this only once per game");
}

setPlayerTurn(goesFirst);
advanceToNextPhase();
onPhaseBegin();

// don't even offer priority, because it's untap of 1st turn now
givePriorityToPlayer = false;

if (startGameHook != null) {
startGameHook.run();
givePriorityToPlayer = true;
}
}

public void discreteStartFirstTurn(Player goesFirst, Runnable startGameHook) {
setupFirstTurn(goesFirst, startGameHook);
mainGameLoop();
}

public void mainGameLoop() {
while (!game.isGameOver() && !(game.getAge() == GameStage.RestartedByKarn)) {
mainLoopStep();
}
}

public void mainLoopStep() {
if (givePriorityToPlayer) {
StopWatch sw = new StopWatch();
if (DEBUG_PHASES) {
sw.start();
}

game.fireEvent(new GameEventPlayerPriority(playerTurn, phase, getPriorityPlayer()));
List<SpellAbility> chosenSa = null;

int loopCount = 0;
do {
if (checkStateBasedEffects()) {
// state-based effects check could lead to game over
return;
}
game.stashGameState();

chosenSa = pPlayerPriority.getController().chooseSpellAbilityToPlay();

// this needs to come after chosenSa so it sees you conceding on own turn
if (playerTurn.hasLost() && pPlayerPriority.equals(playerTurn) && pFirstPriority.equals(playerTurn)) {
// If the active player has lost, and they have priority, set the next player to have priority
System.out.println("Active player is no longer in the game...");
pPlayerPriority = game.getNextPlayerAfter(getPriorityPlayer());
pFirstPriority = pPlayerPriority;
}

if (chosenSa == null) {
break; // that means 'I pass'
}
if (DEBUG_PHASES) {
System.out.print("... " + pPlayerPriority + " plays " + chosenSa);
}

boolean rollback = false;
for (SpellAbility sa : chosenSa) {
Card saHost = sa.getHostCard();
final Zone originZone = saHost.getZone();

if (pPlayerPriority.getController().playChosenSpellAbility(sa)) {
// 117.3c If a player has priority when they cast a spell, activate an ability, [play a land]
// that player receives priority afterward.
pFirstPriority = pPlayerPriority; // all opponents have to pass before stack is allowed to resolve
} else if (game.EXPERIMENTAL_RESTORE_SNAPSHOT) {
rollback = true;
}

saHost = game.getCardState(saHost);
final Zone currentZone = saHost.getZone();

// Need to check if Zone did change
if (currentZone != null && originZone != null && !currentZone.equals(originZone) && (sa.isSpell() || sa instanceof LandAbility)) {
// currently there can be only one Spell put on the Stack at once, or Land Abilities be played
final CardZoneTable triggerList = new CardZoneTable(game.getLastStateBattlefield(), game.getLastStateGraveyard());
triggerList.put(originZone.getZoneType(), currentZone.getZoneType(), saHost);
triggerList.triggerChangesZoneAll(game, sa);
}
}
// Don't copy last state if we're in the middle of rolling back a spell...
if (!rollback) {
game.copyLastState();
}
loopCount++;
} while (loopCount < 999 || !pPlayerPriority.getController().isAI());

if (loopCount >= 999 && pPlayerPriority.getController().isAI()) {
System.out.print("AI looped too much with: " + chosenSa);
}

if (DEBUG_PHASES) {
sw.stop();
System.out.print("... passed in " + sw.getTime()/1000f + " s\n");
System.out.println("\t\tStack: " + game.getStack());
sw.reset();
}
}
else if (DEBUG_PHASES) {
System.out.print(" >> (no priority given to " + getPriorityPlayer() + ")\n");
}

// actingPlayer is the player who may act
// the firstAction is the player who gained Priority First in this segment
// of Priority
Player nextPlayer = game.getNextPlayerAfter(getPriorityPlayer());

if (game.isGameOver() || nextPlayer == null) { return; } // conceded?

if (DEBUG_PHASES) {
System.out.println(TextUtil.concatWithSpace(playerTurn.toString(),TextUtil.addSuffix(phase.toString(),":"), pPlayerPriority.toString(),"is active, previous was", nextPlayer.toString()));
}
if (pFirstPriority == nextPlayer) {
if (game.getStack().isEmpty()) {
if (playerTurn.hasLost()) {
setPriority(game.getNextPlayerAfter(playerTurn));
} else {
setPriority(playerTurn);
}

// end phase
givePriorityToPlayer = true;
onPhaseEnd();
advanceToNextPhase();
onPhaseBegin();
}
else if (!game.getStack().hasSimultaneousStackEntries()) {
game.getStack().resolveStack();
}
} else {
// pass the priority to other player
pPlayerPriority = nextPlayer;
}

// If ever the karn's ultimate resolved
if (game.getAge() == GameStage.RestartedByKarn) {
setPhase(null);
game.updatePhaseForView();
game.fireEvent(new GameEventGameRestarted(playerTurn));
return;
}

// update Priority for all players
for (final Player p : game.getPlayers()) {
p.setHasPriority(getPriorityPlayer() == p);
}

}

public void startFirstTurn(Player goesFirst) {
startFirstTurn(goesFirst, null);
}
public void startFirstTurn(Player goesFirst, Runnable startGameHook) {

if (this.game.EXPERIMENTAL_LOOP) {
System.out.println("Hit feature flag for experimental loop");
discreteStartFirstTurn(goesFirst, startGameHook);
return;
} else {
System.out.println("No feature flag for experimental loop");
}

StopWatch sw = new StopWatch();

if (phase != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ public void initialize() {
lstControls.add(Pair.of(view.getCbEnforceDeckLegality(), FPref.ENFORCE_DECK_LEGALITY));
lstControls.add(Pair.of(view.getCbPerformanceMode(), FPref.PERFORMANCE_MODE));
lstControls.add(Pair.of(view.getCbExperimentalRestore(), FPref.MATCH_EXPERIMENTAL_RESTORE));
lstControls.add(Pair.of(view.getCbExperimentalMainLoop(), FPref.MATCH_EXPERIMENTAL_LOOP));
lstControls.add(Pair.of(view.getCbFilteredHands(), FPref.FILTERED_HANDS));
lstControls.add(Pair.of(view.getCbCloneImgSource(), FPref.UI_CLONE_MODE_SOURCE));
lstControls.add(Pair.of(view.getCbRemoveSmall(), FPref.DECKGEN_NOSMALL));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
private final JCheckBox cbWorkshopSyntax = new OptionsCheckBox(localizer.getMessage("cbWorkshopSyntax"));
private final JCheckBox cbEnforceDeckLegality = new OptionsCheckBox(localizer.getMessage("cbEnforceDeckLegality"));
private final JCheckBox cbExperimentalRestore = new OptionsCheckBox(localizer.getMessage("cbExperimentalRestore"));
private final JCheckBox cbExperimentalMainLoop = new OptionsCheckBox(localizer.getMessage("cbExperimentalMainLoop"));
private final JCheckBox cbPerformanceMode = new OptionsCheckBox(localizer.getMessage("cbPerformanceMode"));
private final JCheckBox cbSROptimize = new OptionsCheckBox(localizer.getMessage("cbSROptimize"));
private final JCheckBox cbFilteredHands = new OptionsCheckBox(localizer.getMessage("cbFilteredHands"));
Expand Down Expand Up @@ -237,6 +238,9 @@ public enum VSubmenuPreferences implements IVSubmenu<CSubmenuPreferences> {
pnlPrefs.add(cbExperimentalRestore, titleConstraints);
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlExperimentalRestore")), descriptionConstraints);

pnlPrefs.add(cbExperimentalMainLoop, titleConstraints);
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlExperimentalMainLoop")), descriptionConstraints);

pnlPrefs.add(cbFilteredHands, titleConstraints);
pnlPrefs.add(new NoteLabel(localizer.getMessage("nlFilteredHands")), descriptionConstraints);

Expand Down Expand Up @@ -849,6 +853,10 @@ public JCheckBox getCbExperimentalRestore() {
return cbExperimentalRestore;
}

public JCheckBox getCbExperimentalMainLoop() {
return cbExperimentalMainLoop;
}

/** @return {@link javax.swing.JCheckBox} */
public JCheckBox getCbFilteredHands() {
return cbFilteredHands;
Expand Down
11 changes: 9 additions & 2 deletions forge-gui-desktop/src/main/java/forge/view/SimulateMatch.java
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ private static void argumentHelp() {
System.out.println("\tq - Quiet flag. Output just the game result, not the entire game log.");
}

public static void simulateSingleMatch(final Match mc, int iGame, boolean outputGamelog) {
public static Game simulateSingleGameOfMatch(final Match mc, int timeout) {
final StopWatch sw = new StopWatch();
sw.start();

Expand All @@ -181,7 +181,7 @@ public static void simulateSingleMatch(final Match mc, int iGame, boolean output
TimeLimitedCodeBlock.runWithTimeout(() -> {
mc.startGame(g1);
sw.stop();
}, 120, TimeUnit.SECONDS);
}, timeout, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.out.println("Stopping slow match as draw");
} catch (Exception | StackOverflowError e) {
Expand All @@ -195,6 +195,13 @@ public static void simulateSingleMatch(final Match mc, int iGame, boolean output
}
}

return g1;
}

public static void simulateSingleMatch(final Match mc, int iGame, boolean outputGamelog) {
final StopWatch sw = new StopWatch();
sw.start();
final Game g1 = simulateSingleGameOfMatch(mc, 120);
List<GameLogEntry> log;
if (outputGamelog) {
log = g1.getGameLog().getLogEntries(null);
Expand Down
60 changes: 60 additions & 0 deletions forge-gui-desktop/src/test/java/forge/GameFuzzingTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

package forge;

import com.google.common.base.Function;
import forge.ai.AIOption;
import forge.ai.LobbyPlayerAi;
import forge.card.CardDb;
import forge.deck.Deck;
import forge.deck.DeckFormat;
import forge.deck.generation.DeckGenerator5Color;
import forge.game.GameRules;
import forge.game.GameType;
import forge.game.Match;
import forge.game.player.RegisteredPlayer;
import forge.gui.GuiBase;
import forge.localinstance.properties.ForgePreferences;
import forge.model.FModel;
import org.testng.annotations.Ignore;
import org.testng.annotations.Test;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

import static forge.view.SimulateMatch.simulateSingleGameOfMatch;

public class GameFuzzingTest {
@Ignore
@Test
public void PlayGameWithRandomDecks() {
GuiBase.setInterface(new GuiDesktop());
FModel.initialize(null, new Function<ForgePreferences, Void>() {
@Override
public Void apply(ForgePreferences preferences) {
preferences.setPref(ForgePreferences.FPref.LOAD_CARD_SCRIPTS_LAZILY, false);
preferences.setPref(ForgePreferences.FPref.UI_LANGUAGE, "en-US");
return null;
}
});

// first deck
CardDb cardDb = FModel.getMagicDb().getCommonCards();
final DeckGenerator5Color gen = new DeckGenerator5Color(cardDb, DeckFormat.Constructed);
final Deck first_deck = new Deck("first", gen.getDeck(60, false));
final Deck second_deck = new Deck("second", gen.getDeck(60, false));

final RegisteredPlayer p1 = new RegisteredPlayer(first_deck);
final RegisteredPlayer p2 = new RegisteredPlayer(second_deck);

Set<AIOption> options = new HashSet<>();
// options.add(AIOption.USE_SIMULATION);
p1.setPlayer(new LobbyPlayerAi("p1", options));
p2.setPlayer(new LobbyPlayerAi("p2", options));
GameRules rules = new GameRules(GameType.Constructed);
// need game rules, players, and title
Match m = new Match(rules, Arrays.asList(p1, p2), "test");

simulateSingleGameOfMatch(m, 120);
}
}
Loading
Loading