Skip to content

Commit

Permalink
Add an end-to-end test that executes an all-AI game to completion. (#…
Browse files Browse the repository at this point in the history
…10522)

Add an end-to-end test that executes an all-AI (hard AI) game to completion.

Currently, only doing this on the very simple "mapmaking tutorial map", which exercises sea zones and amphib invasions. In my testing, this takes 1 min to run and a game completes in 24 rounds.

Also adds a few more maps to GameSaveTest.
  • Loading branch information
asvitkine authored May 28, 2022
1 parent 0a17a36 commit 05850c5
Show file tree
Hide file tree
Showing 5 changed files with 187 additions and 104 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@
import java.util.Set;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.triplea.java.Interruptibles;
import org.triplea.java.ThreadRunner;
Expand All @@ -68,10 +70,11 @@ public class ServerGame extends AbstractGame {
private IRandomSource randomSource = new PlainRandomSource();
private IRandomSource delegateRandomSource;
private final DelegateExecutionManager delegateExecutionManager = new DelegateExecutionManager();
private InGameLobbyWatcherWrapper inGameLobbyWatcher;
@Getter @Setter private InGameLobbyWatcherWrapper inGameLobbyWatcher;
private boolean needToInitialize = true;
private final LaunchAction launchAction;
private final ClientNetworkBridge clientNetworkBridge;
@Setter private boolean delegateAutosavesEnabled = true;

/**
* When the delegate execution is stopped, we countdown on this latch to prevent the
Expand All @@ -81,6 +84,9 @@ public class ServerGame extends AbstractGame {
/** Has the delegate signaled that delegate execution should stop. */
private volatile boolean delegateExecutionStopped = false;

// If true, setting delegateExecutionStopped to true will stop the game (return from startGame()).
@Setter private boolean stopGameOnDelegateExecutionStop = false;

public ServerGame(
final GameData data,
final Set<Player> localPlayers,
Expand Down Expand Up @@ -295,9 +301,13 @@ public void startGame() {
}
while (!isGameOver) {
if (delegateExecutionStopped) {
// the delegate has told us to stop stepping through game steps
// don't let this method return, as this method returning signals that the game is over.
Interruptibles.await(delegateExecutionStoppedLatch);
if (stopGameOnDelegateExecutionStop) {
stopGame();
} else {
// the delegate has told us to stop stepping through game steps
// don't let this method return, as this method returning signals that the game is over.
Interruptibles.await(delegateExecutionStoppedLatch);
}
} else {
runStep(false);
}
Expand Down Expand Up @@ -400,15 +410,11 @@ private void runStep(final boolean stepIsRestoredFromSavedGame) {
}
final GameStep currentStep = gameData.getSequence().getStep();
final IDelegate currentDelegate = currentStep.getDelegate();
if (!stepIsRestoredFromSavedGame
&& currentDelegate.getClass().isAnnotationPresent(AutoSave.class)
&& currentDelegate.getClass().getAnnotation(AutoSave.class).beforeStepStart()) {
if (!stepIsRestoredFromSavedGame && shouldAutoSaveBeforeStart(currentDelegate)) {
autoSaveBefore(currentDelegate);
}
startStep(stepIsRestoredFromSavedGame);
if (!stepIsRestoredFromSavedGame
&& currentDelegate.getClass().isAnnotationPresent(AutoSave.class)
&& currentDelegate.getClass().getAnnotation(AutoSave.class).afterStepStart()) {
if (!stepIsRestoredFromSavedGame && shouldAutoSaveAfterStart(currentDelegate)) {
autoSaveBefore(currentDelegate);
}
if (isGameOver) {
Expand All @@ -418,17 +424,10 @@ private void runStep(final boolean stepIsRestoredFromSavedGame) {
if (isGameOver) {
return;
}
// save after the step has advanced
// otherwise, the delegate will execute again.
final boolean autoSaveThisDelegate =
currentDelegate.getClass().isAnnotationPresent(AutoSave.class)
&& currentDelegate.getClass().getAnnotation(AutoSave.class).afterStepEnd();
if (autoSaveThisDelegate && currentStep.getName().endsWith("Move")) {
final String stepName = currentStep.getName();
// If we are headless we don't want to include the nation in the save game because that would
// make it too
// difficult to load later.
autoSaveAfter(stepName);
// save after the step has advanced otherwise, the delegate will execute again.
boolean isMoveStep = GameStep.isMoveStep(currentStep.getName());
if (isMoveStep && shouldAutoSaveAfterEnd(currentDelegate)) {
autoSaveAfter(currentStep.getName());
}
endStep();
if (isGameOver) {
Expand All @@ -441,11 +440,29 @@ private void runStep(final boolean stepIsRestoredFromSavedGame) {
? launchAction.getAutoSaveFileUtils().getEvenRoundAutoSaveFile()
: launchAction.getAutoSaveFileUtils().getOddRoundAutoSaveFile());
}
if (autoSaveThisDelegate && !currentStep.getName().endsWith("Move")) {
if (!isMoveStep && shouldAutoSaveAfterEnd(currentDelegate)) {
autoSaveAfter(currentDelegate);
}
}

private boolean shouldAutoSaveBeforeStart(IDelegate delegate) {
return delegateAutosavesEnabled
&& delegate.getClass().isAnnotationPresent(AutoSave.class)
&& delegate.getClass().getAnnotation(AutoSave.class).beforeStepStart();
}

private boolean shouldAutoSaveAfterStart(IDelegate delegate) {
return delegateAutosavesEnabled
&& delegate.getClass().isAnnotationPresent(AutoSave.class)
&& delegate.getClass().getAnnotation(AutoSave.class).afterStepStart();
}

private boolean shouldAutoSaveAfterEnd(IDelegate delegate) {
return delegateAutosavesEnabled
&& delegate.getClass().isAnnotationPresent(AutoSave.class)
&& delegate.getClass().getAnnotation(AutoSave.class).afterStepEnd();
}

private void autoSaveAfter(final String stepName) {
final var saveUtils = launchAction.getAutoSaveFileUtils();
saveGame(saveUtils.getAfterStepAutoSaveFile(saveUtils.getAutoSaveStepName(stepName)));
Expand Down Expand Up @@ -660,14 +677,6 @@ public void setRandomSource(final IRandomSource randomSource) {
delegateRandomSource = null;
}

public InGameLobbyWatcherWrapper getInGameLobbyWatcher() {
return inGameLobbyWatcher;
}

public void setInGameLobbyWatcher(final InGameLobbyWatcherWrapper inGameLobbyWatcher) {
this.inGameLobbyWatcher = inGameLobbyWatcher;
}

public void stopGameSequence() {
delegateExecutionStopped = true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,8 +316,7 @@ public void signalGameOver(
final boolean stopGame;
if (HeadlessGameServer.headless()) {
// a terrible dirty hack, but I can't think of a better way to do it right now. If we are
// headless, end the
// game.
// headless, end the game.
stopGame = true;
} else {
// now tell the HOST, and see if they want to continue the game.
Expand All @@ -331,8 +330,7 @@ public void signalGameOver(
displayMessage = displayMessage + "</br><p>Do you want to continue?</p>";
}
// this is currently the ONLY instance of JOptionPane that is allowed outside of the UI
// classes. maybe there is
// a better way?
// classes. maybe there is a better way?
stopGame =
!EventThreadJOptionPane.showConfirmDialog(
null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package games.strategy.engine.data;

import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.empty;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.Is.is;

import games.strategy.engine.framework.ServerGame;
import games.strategy.engine.framework.startup.ui.panels.main.game.selector.GameSelectorModel;
import games.strategy.triplea.delegate.EndRoundDelegate;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

/**
* End-to-end test that starts all-AI games on a selected set of maps and verifies they conclude
* with a victory for one of the sides. This ensures the AI code doesn't run into errors and can
* make progress towards victory conditions.
*/
@Slf4j
public class AiGameTest {
@BeforeAll
public static void setUp() throws IOException {
GameTestUtils.setUp();
}

@ParameterizedTest
@CsvSource({
"map_making_tutorial,map/games/Test1.xml",
})
void testAiGame(String mapName, String mapXmlPath) throws Exception {
GameSelectorModel gameSelector = GameTestUtils.loadGameFromURI(mapName, mapXmlPath);
ServerGame game = GameTestUtils.setUpGameWithAis(gameSelector);
game.setStopGameOnDelegateExecutionStop(true);
game.startGame();
assertThat(game.isGameOver(), is(true));
assertThat(game.getData().getSequence().getRound(), greaterThan(2));
EndRoundDelegate endDelegate = (EndRoundDelegate) game.getData().getDelegate("endRound");
assertThat(endDelegate.getWinners(), not(empty()));
log.info("Game completed at round: " + game.getData().getSequence().getRound());
log.info("Game winners: " + endDelegate.getWinners());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,15 @@
import static org.hamcrest.Matchers.not;
import static org.hamcrest.io.FileMatchers.aFileWithSize;

import games.strategy.engine.ClientFileSystemHelper;
import games.strategy.engine.framework.GameDataFileUtils;
import games.strategy.engine.framework.ServerGame;
import games.strategy.engine.framework.map.file.system.loader.ZippedMapsExtractor;
import games.strategy.engine.framework.startup.ui.PlayerTypes;
import games.strategy.engine.framework.startup.ui.panels.main.game.selector.GameSelectorModel;
import games.strategy.engine.player.Player;
import games.strategy.net.LocalNoOpMessenger;
import games.strategy.net.Messengers;
import games.strategy.net.websocket.ClientNetworkBridge;
import games.strategy.triplea.settings.ClientSetting;
import java.io.IOException;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.triplea.config.product.ProductVersionReader;
import org.triplea.game.server.HeadlessLaunchAction;
import org.triplea.injection.Injections;
import org.triplea.io.ContentDownloader;
import org.triplea.io.FileUtils;

/**
* Checks that no error is encountered when saving a game on several different maps. This test
Expand All @@ -42,67 +24,24 @@
* example, if there's a non-null reference to a non-serializable object.
*/
class GameSaveTest {

@BeforeAll
public static void setUp() throws IOException {
Injections.init(
Injections.builder()
.engineVersion(new ProductVersionReader().getVersion())
.playerTypes(PlayerTypes.getBuiltInPlayerTypes())
.build());
ClientSetting.initialize();
final Path tempFolder = FileUtils.newTempFolder();
FileUtils.writeToFile(tempFolder.resolve(".triplea-root"), "");
Files.createDirectory(tempFolder.resolve("assets"));
ClientFileSystemHelper.setCodeSourceFolder(tempFolder);
GameTestUtils.setUp();
}

@ParameterizedTest
@CsvSource({
"map_making_tutorial,map/games/Test1.xml",
"minimap,map/games/minimap.xml",
"world_war_ii_revised,map/games/ww2v2.xml",
"pacific_challenge,map/games/Pacific_Theater_Solo_Challenge.xml"
"pacific_challenge,map/games/Pacific_Theater_Solo_Challenge.xml",
"imperialism_1974_board_game,map/games/imperialism_1974_board_game.xml",
})
void testSaveGame(final String mapName, final String mapXmlPath) throws Exception {
final Path mapFolderPath = downloadMap(getMapDownloadURI(mapName));
final GameSelectorModel gameSelector = new GameSelectorModel();
gameSelector.load(mapFolderPath.resolve(mapXmlPath));
final ServerGame game = startGameWithAis(gameSelector);
final Path saveFile = Files.createTempFile("save", GameDataFileUtils.getExtension());
void testSaveGame(String mapName, String mapXmlPath) throws Exception {
GameSelectorModel gameSelector = GameTestUtils.loadGameFromURI(mapName, mapXmlPath);
ServerGame game = GameTestUtils.setUpGameWithAis(gameSelector);
Path saveFile = Files.createTempFile("save", GameDataFileUtils.getExtension());
game.saveGame(saveFile);
assertThat(saveFile.toFile(), is(not(aFileWithSize(0))));
}

private static Path downloadMap(final URI uri) throws IOException {
final Path targetTempFileToDownloadTo = FileUtils.newTempFolder().resolve("map.zip");
try (ContentDownloader downloader = new ContentDownloader(uri)) {
Files.copy(downloader.getStream(), targetTempFileToDownloadTo);
}
return ZippedMapsExtractor.unzipMap(targetTempFileToDownloadTo).get();
}

private static URI getMapDownloadURI(final String mapName) throws URISyntaxException {
return new URI(String.format("https://github.com/triplea-maps/%s/archive/master.zip", mapName));
}

private static ServerGame startGameWithAis(final GameSelectorModel gameSelector) {
final GameData gameData = gameSelector.getGameData();
final Map<String, PlayerTypes.Type> playerTypes = new HashMap<>();
for (var player : gameData.getPlayerList().getPlayers()) {
playerTypes.put(player.getName(), PlayerTypes.PRO_AI);
}
final Set<Player> gamePlayers = gameData.getGameLoader().newPlayers(playerTypes);
final HeadlessLaunchAction launchAction = new HeadlessLaunchAction();
final Messengers messengers = new Messengers(new LocalNoOpMessenger());
final ServerGame game =
new ServerGame(
gameData,
gamePlayers,
new HashMap<>(),
messengers,
ClientNetworkBridge.NO_OP_SENDER,
launchAction);
// Note: This doesn't actually start the AI players' turns. For that, call game.startGame().
gameData.getGameLoader().startGame(game, gamePlayers, launchAction, null);
return game;
}
}
Loading

0 comments on commit 05850c5

Please sign in to comment.