Content of file IterationalGame.java /*
* Copyright © 2020 Fachhochschule für die Wirtschaft (FHDW) Hannover
*
* This file is part of gaming-contest.
*
* Gaming-contest is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* Gaming-contest is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* gaming-contest. If not, see <http://www.gnu.org/licenses/>.
*/
package de.fhdw.gaming.ipspiel21.iterationalContest;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.EnumMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.stream.Collectors;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.DefaultParser;
import org.apache.commons.cli.Options;
import de.fhdw.gaming.contest.RecordingObserver;
import de.fhdw.gaming.contest.ui.InteractiveStreamInputProvider;
import de.fhdw.gaming.contest.util.CombinatoricsHelper;
import de.fhdw.gaming.contest.util.ComparablePair;
import de.fhdw.gaming.core.domain.DefaultGameBuilderFactoryProvider;
import de.fhdw.gaming.core.domain.Game;
import de.fhdw.gaming.core.domain.GameBuilder;
import de.fhdw.gaming.core.domain.GameBuilderFactory;
import de.fhdw.gaming.core.domain.GameException;
import de.fhdw.gaming.core.domain.Player;
import de.fhdw.gaming.core.domain.PlayerState;
import de.fhdw.gaming.core.domain.Strategy;
import de.fhdw.gaming.core.domain.util.GameBuilderFactoryWrapper;
import de.fhdw.gaming.core.ui.InputProvider;
import de.fhdw.gaming.core.ui.InputProviderException;
import de.fhdw.gaming.core.ui.type.validator.MaxValueValidator;
import de.fhdw.gaming.core.ui.type.validator.MinValueValidator;
import de.fhdw.gaming.core.ui.util.ChainedInputProvider;
import de.fhdw.gaming.core.ui.util.NonInteractiveInputProvider;
import de.fhdw.gaming.ipspiel21.evolution.EvolutionPlayer;
import de.fhdw.gaming.ipspiel21.evolution.GameHistoryCollection;
import de.fhdw.gaming.ipspiel21.evolution.MemoryObserver;
/**
* Provides the main entry point.
*/
public final class IterationalGame {
/**
* Type of game.
*/
private static final String PARAM_GAME = "iterationalGame";
/**
* The number of games to be played.
*/
private static final String PARAM_REPEAT_COUNT = "repeatCount";
/**
* If non-null, the events of all contests are recorded into this directory.
*/
private final File recordingDirectory;
/**
* Constructor.
*
* @param recordingDirectory If non-null, the events of all contests are recorded into this directory.
*/
public IterationalGame(final String recordingDirectory) {
this.recordingDirectory = recordingDirectory != null ? new File(recordingDirectory) : null;
}
/**
* Runs the contest.
*/
public void run() throws InputProviderException, GameException, InterruptedException, IOException {
final CompetitionParameters competitionParameters = this.determineCompetitionParameters();
final CompetitionResults competitionResults = this.runCompetition(competitionParameters);
System.out.println();
final int numberOfGamesPlayed = competitionResults.getTotalNumberOfGamesPlayed();
System.out.println(String.format("A total of %d games played. Results:", numberOfGamesPlayed));
this.printResult(numberOfGamesPlayed, competitionResults.getGameResults());
}
/**
* Prints the result of the competition.
*
* @param numberOfGamesPlayed The number of games played.
* @param gameResults The game results.
*/
private void printResult(final int numberOfGamesPlayed, final Map<Class<?>, StrategyData> gameResults) {
final SortedMap<ComparablePair<Integer, Integer>, List<StrategyData>> ranking = new TreeMap<>();
final Comparator<Integer> reverseCompare = ((Comparator<Integer>) Integer::compareTo).reversed();
int maxNameLength = 0;
int maxOutcomeLength = 0;
for (final StrategyData strategyData : gameResults.values()) {
final String name = strategyData.getName();
// first order by games won, then (giving equality) by games that ended in a draw
final ComparablePair<Integer,
Integer> key = ComparablePair.of(
strategyData.getCounters().getOrDefault(PlayerState.WON, 0),
reverseCompare,
strategyData.getCounters().getOrDefault(PlayerState.DRAW, 0),
reverseCompare);
ranking.computeIfAbsent(key, (final var k) -> new ArrayList<>()).add(strategyData);
maxNameLength = Math.max(maxNameLength, name.length());
maxOutcomeLength = Math
.max(maxOutcomeLength, String.format("%.3f", strategyData.getTotalOutcome()).length());
}
final int maxNumFieldWidth = String.valueOf(numberOfGamesPlayed).length();
int place = 1;
for (final Map.Entry<ComparablePair<Integer, Integer>, List<StrategyData>> rankingEntry : ranking.entrySet()) {
final List<StrategyData> strategyDataList = rankingEntry.getValue();
boolean first = true;
for (final StrategyData strategyData : strategyDataList) {
System.out.println(
String.format(
String.format(
" %%s Strategy %%-%1$ds "
+ "[WON: %%%2$dd, DRAW: %%%2$dd, LOST: %%%2$dd, RESIGNED: %%%2$dd] "
+ "[OUTCOME: %%%3$d.3f]",
maxNameLength,
maxNumFieldWidth,
maxOutcomeLength),
String.format(first ? "%4d. place:" : " ", place),
strategyData.getName(),
rankingEntry.getKey().getFirst(),
rankingEntry.getKey().getSecond(),
strategyData.getCounters().getOrDefault(PlayerState.LOST, 0),
strategyData.getCounters().getOrDefault(PlayerState.RESIGNED, 0),
strategyData.getTotalOutcome()));
first = false;
}
place += strategyDataList.size();
}
}
/**
* Determines the competition parameters.
*/
private CompetitionParameters determineCompetitionParameters() throws InputProviderException, GameException {
final Set<GameBuilderFactory> gameBuilderFactories = new LinkedHashSet<>();
for (final GameBuilderFactory gameBuilderFactory : new DefaultGameBuilderFactoryProvider()
.getGameBuilderFactories()) {
gameBuilderFactories.add(new GameBuilderFactoryWrapper(gameBuilderFactory));
}
final InputProvider inputProvider = new InteractiveStreamInputProvider(System.in, System.out)
.needObject(IterationalGame.PARAM_GAME, "Which game to play", Optional.empty(), gameBuilderFactories)
.needInteger(
IterationalGame.PARAM_REPEAT_COUNT,
"Number of games per contest",
Optional.of(100),
new MinValueValidator<>(1),
new MaxValueValidator<>(1000))
.needInteger(
GameBuilderFactory.PARAM_MAX_COMPUTATION_TIME_PER_MOVE,
"Maximum computation time in seconds per player and move",
Optional.of(GameBuilder.DEFAULT_MAX_COMPUTATION_TIME_PER_MOVE),
new MinValueValidator<>(1));
if (gameBuilderFactories.size() == 1) {
inputProvider.fixedObject(IterationalGame.PARAM_GAME, gameBuilderFactories.iterator().next());
}
final Map<String, Object> data = inputProvider.requestData("Competition parameters");
final GameBuilderFactory gameBuilderFactory = (GameBuilderFactory) data.get(IterationalGame.PARAM_GAME);
final int repeatCount = (Integer) data.get(IterationalGame.PARAM_REPEAT_COUNT);
final int maxComputationTimePerMove = (Integer) data
.get(GameBuilderFactory.PARAM_MAX_COMPUTATION_TIME_PER_MOVE);
return new CompetitionParameters(gameBuilderFactory, repeatCount, maxComputationTimePerMove);
}
/**
* Runs a competition.
*
* @param competitionParameters The competition parameters.
* @return A map where to store how the strategies have performed in the competition.
*/
private CompetitionResults runCompetition(final CompetitionParameters competitionParameters)
throws InputProviderException, GameException, InterruptedException, IOException {
final CompetitionResults competitionResults = new CompetitionResults();
final GameBuilderFactory gameBuilderFactory = competitionParameters.getGameBuilderFactory();
final List<Strategy<?, ?, ?>> strategies = new ArrayList<>(gameBuilderFactory.getStrategies());
// a competition must run non-interactively, so we have to filter out interactive strategies
strategies.removeIf(Strategy::isInteractive);
final Comparator<Strategy<?, ?, ?>> comp = (final Strategy<?, ?, ?> l, final Strategy<?, ?, ?> r) -> Integer
.compare(strategies.indexOf(l), strategies.indexOf(r));
int contestNumber = 1;
for (int numPlayers = gameBuilderFactory.getMinimumNumberOfPlayers();
numPlayers <= gameBuilderFactory.getMaximumNumberOfPlayers(); ++numPlayers) {
for (final List<Strategy<?, ?, ?>> strategySet : CombinatoricsHelper
.combinationsWithRepetition(strategies, numPlayers)
.flatMap(
(final List<Strategy<?, ?, ?>> strategyList) -> CombinatoricsHelper
.permutationsWithRepetition(strategyList, comp))
.collect(Collectors.toList())) {
System.out.println(
String.format(
"Contest %d. Number of players: %d. Strategies involved: %s",
contestNumber,
numPlayers,
strategySet));
final ContestParameters contestParameters = this
.determineContestParameters(competitionParameters, strategySet);
this.runContest(contestNumber, contestParameters, competitionResults);
++contestNumber;
}
}
return competitionResults;
}
/**
* Determines the contest parameters.
*
* @param competitionParameters The competition parameters.
* @param strategies A set of strategies to be used for this contest.
*/
private ContestParameters determineContestParameters(final CompetitionParameters competitionParameters,
final List<? extends Strategy<?, ?, ?>> strategies) throws InputProviderException, GameException {
final ListIterator<? extends Strategy<?, ?, ?>> itStrategy = strategies.listIterator(strategies.size());
InputProvider mostRecentProvider = null;
int nextPlayerId = strategies.size();
while (itStrategy.hasPrevious()) {
final InputProvider lastPlayerProvider = mostRecentProvider;
final InputProvider playerInputProvider = lastPlayerProvider == null ? new NonInteractiveInputProvider()
: new ChainedInputProvider(
new NonInteractiveInputProvider(),
(final Map<String, Object> lastDataSet) -> lastPlayerProvider);
playerInputProvider.fixedString(GameBuilderFactory.PARAM_PLAYER_NAME, Integer.toString(nextPlayerId--));
playerInputProvider.fixedObject(GameBuilderFactory.PARAM_PLAYER_STRATEGY, itStrategy.previous());
mostRecentProvider = playerInputProvider;
}
final InputProvider firstPlayerProvider = mostRecentProvider;
final InputProvider gameInputProvider = firstPlayerProvider == null ? new NonInteractiveInputProvider()
: new ChainedInputProvider(
new NonInteractiveInputProvider(),
(final Map<String, Object> lastDataSet) -> firstPlayerProvider);
gameInputProvider.fixedInteger(
GameBuilderFactory.PARAM_MAX_COMPUTATION_TIME_PER_MOVE,
competitionParameters.getMaxComputationTimePerMove());
final GameBuilder gameBuilder = competitionParameters.getGameBuilderFactory()
.createGameBuilder(gameInputProvider);
return new ContestParameters(gameBuilder, competitionParameters.getNumberOfGamesPerContest());
}
/**
* Runs a contest.
*
* @param contestNumber The number of the contest.
* @param contestParameters The contest parameters.
* @param competitionResults The competition results where to store how the strategies have performed in the
* competition.
*/
private void runContest(final int contestNumber, final ContestParameters contestParameters,
final CompetitionResults competitionResults) throws GameException, InterruptedException, IOException {
final File contestDirectory;
if (this.recordingDirectory != null) {
contestDirectory = new File(this.recordingDirectory, String.format("%d", contestNumber));
} else {
contestDirectory = null;
}
final Map<Class<?>, StrategyData> gameResults = competitionResults.getGameResults();
final GameBuilder gameBuilder = contestParameters.getGameBuilder();
Optional<GameHistoryCollection> memoryFirstPlayer = Optional.empty();
Optional<GameHistoryCollection> memorySecondPlayer = Optional.empty();
for (int i = 1; i <= contestParameters.getNumberOfGames(); ++i) {
RecordingObserver observer = null;
final MemoryObserver memobserver = new MemoryObserver();
try (Game<?, ?, ?, ?> game = gameBuilder.build(i)) {
if (contestDirectory != null) {
final File gameEventFile = createGameEventFile(contestDirectory, String.format("%d.log", i));
observer = new RecordingObserver(gameEventFile);
game.addObserver(observer);
}
game.addObserver(memobserver);
final List<Player> players = new ArrayList<>(game.getState().getPlayers().values());
this.setMemoryToPlayer(players.get(0), memoryFirstPlayer);
this.setMemoryToPlayer(players.get(1), memorySecondPlayer);
game.start();
while (!game.isFinished()) {
game.makeMove();
}
memoryFirstPlayer = this.getMemoryFromPlayer(players.get(0));
memorySecondPlayer = this.getMemoryFromPlayer(players.get(1));
competitionResults.incrementTotalNumberOfGamesPlayed();
System.out.print('.');
final Map<String, ? extends Strategy<?, ?, ?>> strategies = game.getStrategies();
for (final Player player : game.getPlayers().values()) {
final Strategy<?, ?, ?> strategy = strategies.get(player.getName());
final Class<?> strategyClass = strategy.getClass();
final StrategyData strategyData = gameResults.computeIfAbsent(
strategyClass,
(final var k) -> new StrategyData(strategy.toString(), new EnumMap<>(PlayerState.class)));
strategyData.getCounters().compute(
player.getState(),
(final PlayerState key, final Integer value) -> value == null ? 1 : value + 1);
strategyData.addOutcome(player.getOutcome().orElseThrow());
}
} finally {
if (observer != null) {
observer.close();
}
}
Java 7 introduced the try-with-resources statement. This statement ensures that each resource is closed at the end
of the statement. It avoids the need of explicitly closing the resources in a finally block. Additionally exceptions
are better handled: If an exception occurred both in the `try` block and `finally` block, then the exception from
the try block was suppressed. With the `try`-with-resources statement, the exception thrown from the try-block is
preserved.
public class TryWithResources {
public void run() {
InputStream in = null;
try {
in = openInputStream();
int i = in.read();
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (in != null) in.close();
} catch (IOException ignored) {
// ignored
}
}
// better use try-with-resources
try (InputStream in2 = openInputStream()) {
int i = in2.read();
}
}
}
See PMD documentation.
}
System.out.println();
}
/**
* Returns the memory of the provided player.
*
* @param player
* @return The memory of the given player
*/
Optional<GameHistoryCollection> getMemoryFromPlayer(final Player player) {
if (player instanceof EvolutionPlayer) {
final EvolutionPlayer ev = (EvolutionPlayer) player;
return Optional.of(ev.getGameHistoryCollection());
} else {
return Optional.empty();
}
}
/**
* Sets the provided memory to the provided player.
*
* @param player
* @param memory
*/
void setMemoryToPlayer(final Player player, final Optional<GameHistoryCollection> memory) {
if (memory.isPresent()) {
final EvolutionPlayer ev = (EvolutionPlayer) player;
ev.setGameHistoryCollection(memory.get());
}
}
/**
* Returns a {@link File} object for storing game events.
*
* @param parentDirectory The parent directory. May be {@code null} if game events shall not be saved.
* @param fileName The file name.
* @return An appropriate {@link File} object or {@code null} if game events shall not be saved.
* @throws IOException if creating the parent directory (if not present yet) has failed.
*/
private static File createGameEventFile(final File parentDirectory, final String fileName) throws IOException {
if (parentDirectory == null) {
return null;
}
if (!parentDirectory.exists() && !parentDirectory.mkdirs()) {
throw new IOException(
String.format(
"Could not create directory for recording events: %s",
parentDirectory.getCanonicalPath()));
}
if (!parentDirectory.isDirectory()) {
throw new IOException(
String.format(
"Directory for recording events is not a directory: %s",
parentDirectory.getCanonicalPath()));
}
return new File(parentDirectory, fileName);
}
/**
* The main entry point.
*
* @param parameters The parameters passed to the application.
*/
public static void main(final String[] parameters) {
final Options options = new Options();
options.addOption("r", "recordDir", true, "Specifies the directory where to record events");
try {
final CommandLine commandLine = new DefaultParser().parse(options, parameters);
new IterationalGame(commandLine.getOptionValue('r')).run();
} catch (final Exception e) {
System.err.print("Game contest aborted due to exception: ");
e.printStackTrace(System.err);
System.exit(3);
}
}
}