Commit 4108a1d3 authored by Tobias MEGGENDORFER's avatar Tobias MEGGENDORFER
Browse files

ugly fix

parent 78646021
package com.cges;
import static com.google.common.base.Preconditions.checkArgument;
import static picocli.CommandLine.ArgGroup;
import static picocli.CommandLine.Command;
import static picocli.CommandLine.Option;
import com.cges.algorithm.RunGraphSolver;
import com.cges.graph.FormulaHistoryGame;
......@@ -20,95 +23,125 @@ import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.PrintStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.annotation.Nullable;
import picocli.CommandLine;
public final class Main {
private Main() {}
public static void main(String[] args) throws IOException {
Stopwatch overall = Stopwatch.createStarted();
ConcurrentGame<?> game;
@Command(name = "cges", mixinStandardHelpOptions = true, version = "Concurrent Game Equilibrium Solver 0.1",
description = "Computes Nash-equilibria for concurrent games")
public final class Main implements Callable<Void> {
private static final Logger log = Logger.getLogger(Main.class.getName());
private static PrintStream open(String output) throws IOException {
return "-".equals(output) ? System.out : new PrintStream(new BufferedOutputStream(Files.newOutputStream(Path.of(output))));
}
private static <S> void writeIfPresent(@Nullable String output, S object, BiConsumer<S, PrintStream> formatter) throws IOException {
if (output != null) {
try (var stream = open(output)) {
formatter.accept(object, stream);
}
}
}
@Nullable
@Option(names = {"--write-dot-cg"}, description = "Write the concurrent game")
private String writeDotConcurrentGame;
@Nullable
@Option(names = {"--write-dot-hg"}, description = "Write the history game")
private String writeDotHistoryGame;
@Nullable
@Option(names = {"--write-dot-sg"}, description = "Write the suspect game")
private String writeDotSuspectGame;
@Nullable
@Option(names = {"--write-dot-sol"}, description = "Write the solutions")
private String writeDotSolution;
@Option(names = {"--write-dot-module"}, description = "Write the module (format: <name>,<destination>)")
private List<String> writeModule = List.of();
@Option(names = {"-O", "--output"}, description = "Write the assignments with nash equilibria")
private String writeOutput = "-";
@Option(names = {"--rg-solver"}, description = "Solver to search for a lasso. Valid: ${COMPLETION-CANDIDATES}, default: ${DEFAULT-VALUE}")
private RunGraphSolver.LassoSolver solver = RunGraphSolver.LassoSolver.GRAPH_SEARCH;
@ArgGroup(heading = "game", multiplicity = "1")
private GameSource gameSource;
static class GameSource {
@Nullable
Set<Map<Agent, Boolean>> validationSet;
if ("game".equals(args[0])) {
try (BufferedReader reader = Files.newBufferedReader(Path.of(args[1]))) {
game = GameParser.parseExplicit(reader.lines());
@Option(names = "--game", description = "Source file in JSON format")
private String json;
@Nullable
@Option(names = "--game-explicit", description = "Source file in explicit format")
private String explicit;
}
private Main() {}
private record Input<S>(ConcurrentGame<S> game, @Nullable Set<Map<Agent, Boolean>> validationSet) {}
public static void main(String[] args) {
System.exit(new CommandLine(new Main())
.setCaseInsensitiveEnumValuesAllowed(true)
.execute(args));
}
private Input<?> parseGame() throws IOException {
if (gameSource.explicit == null) {
assert gameSource.json != null;
JsonObject jsonObject;
try (BufferedReader reader = Files.newBufferedReader(Path.of(gameSource.json))) {
jsonObject = JsonParser.parseReader(reader).getAsJsonObject();
}
validationSet = null;
} else {
try (BufferedReader reader = Files.newBufferedReader(Path.of(args[0]))) {
JsonObject jsonObject = JsonParser.parseReader(reader).getAsJsonObject();
game = GameParser.parse(jsonObject);
if (game instanceof ModuleGame<?> module) {
for (Module<?> m : module.modules()) {
DotWriter.writeModule(m, module, System.out);
System.out.println();
}
}
JsonArray validation = jsonObject.getAsJsonArray("expected");
if (validation == null) {
validationSet = null;
} else {
validationSet = new HashSet<>();
for (JsonElement jsonElement : validation) {
Map<Agent, Boolean> expectedResult = new HashMap<>();
for (Map.Entry<String, JsonElement> entry : jsonElement.getAsJsonObject().entrySet()) {
Agent agent = game.agent(entry.getKey());
boolean payoff = entry.getValue().getAsBoolean();
expectedResult.put(agent, payoff);
}
checkArgument(expectedResult.keySet().equals(game.agents()), "Invalid validation specification");
validationSet.add(expectedResult);
}
}
var game = GameParser.parse(jsonObject);
JsonArray validation = jsonObject.getAsJsonArray("expected");
if (validation == null) {
return new Input<>(game, null);
}
}
var list = analyse(game).peek(solution -> System.out.printf("Found NE for %s:%n%s%n",
Formatter.format(solution.assignment(), game),
solution.strategy())).collect(Collectors.toList());
System.out.println("Overall: " + overall);
for (GameSolution<?> gameSolution : list) {
DotWriter.writeSolution(gameSolution, System.out);
System.out.println();
System.out.println();
System.out.println();
}
list.sort(Comparator.comparingLong(solution -> game.agents().stream().map(solution.assignment()::isLoser).count()));
list.stream().map(solution -> Formatter.format(solution.assignment(), game)).forEach(System.out::println);
if (validationSet != null) {
Set<Map<Agent, Boolean>> results = list.stream().map(GameSolution::assignment)
.map(p -> game.agents().stream().collect(Collectors.toUnmodifiableMap(Function.identity(), p::isWinner)))
.collect(Collectors.toUnmodifiableSet());
if (!results.equals(validationSet)) {
var agents = game.agents().stream().sorted(Comparator.comparing(Agent::name)).toList();
System.out.println("Expected equilibrium:");
for (Map<Agent, Boolean> map : validationSet) {
System.out.println(agents.stream().map(a -> "%s:%s".formatted(a.name(), map.get(a))).collect(Collectors.joining(",", "[", "]")));
Set<Map<Agent, Boolean>> validationSet = new HashSet<>();
for (JsonElement jsonElement : validation) {
Map<Agent, Boolean> expectedResult = new HashMap<>();
for (Map.Entry<String, JsonElement> entry : jsonElement.getAsJsonObject().entrySet()) {
Agent agent = game.agent(entry.getKey());
boolean payoff = entry.getValue().getAsBoolean();
expectedResult.put(agent, payoff);
}
System.out.println("Found equilibrium:");
for (Map<Agent, Boolean> map : results) {
System.out.println(agents.stream().map(a -> "%s:%s".formatted(a.name(), map.get(a))).collect(Collectors.joining(",", "[", "]")));
}
System.exit(1);
checkArgument(expectedResult.keySet().equals(game.agents()), "Invalid validation specification");
validationSet.add(expectedResult);
}
return new Input<>(game, validationSet);
}
try (BufferedReader reader = Files.newBufferedReader(Path.of(gameSource.explicit))) {
return new Input<>(GameParser.parseExplicit(reader.lines()), null);
}
}
private static <S> Stream<GameSolution<S>> analyse(ConcurrentGame<S> game) {
var suspectGame = new SuspectGame<>(new FormulaHistoryGame<>(game));
private <S> Stream<GameSolution<S>> computeSolutions(ConcurrentGame<S> game) throws IOException {
var historyGame = new FormulaHistoryGame<>(game);
writeIfPresent(writeDotHistoryGame, historyGame, DotWriter::writeHistoryGame);
var suspectGame = new SuspectGame<>(historyGame);
writeIfPresent(writeDotSuspectGame, suspectGame, DotWriter::writeSuspectGame);
Set<Agent> undefinedAgents = game.agents().stream()
.filter(a -> a.payoff().equals(Agent.Payoff.UNDEFINED))
......@@ -116,13 +149,93 @@ public final class Main {
return Sets.powerSet(undefinedAgents).stream()
.map(PayoffAssignment::new)
.map(payoff -> {
System.out.printf("Processing: %s%n", Formatter.format(payoff, game));
log.log(Level.INFO, () -> "Processing: %s".formatted(Formatter.format(payoff, game)));
Stopwatch timer = Stopwatch.createStarted();
RunGraph<S> runGraph = new RunGraph<>(suspectGame, payoff);
var strategy = RunGraphSolver.solve(runGraph);
System.out.println("Solution: " + timer);
var strategy = RunGraphSolver.solve(runGraph, solver);
log.log(Level.INFO, () -> "Solution: %s".formatted(timer));
return strategy.map(s -> new GameSolution<>(suspectGame, runGraph, payoff, s));
}).flatMap(Optional::stream);
}
private <S> List<GameSolution<S>> solve(Input<S> input) throws IOException {
Stopwatch overall = Stopwatch.createStarted();
var game = input.game;
var solutionList = computeSolutions(game)
.peek(solution -> log.log(Level.INFO,
() -> "Found NE for %s:%n%s%n".formatted(Formatter.format(solution.assignment(), game), solution.strategy())))
.toList();
log.log(Level.INFO, () -> "Solving took %s overall".formatted(overall));
if (!solutionList.isEmpty()) {
writeIfPresent(writeDotSolution, solutionList, (list, stream) -> {
for (GameSolution<?> solution : list) {
DotWriter.writeSolution(solution, stream);
stream.println();
}
});
}
if (input.validationSet != null) {
validate(game, solutionList, input.validationSet);
}
return solutionList;
}
@Override
public Void call() throws Exception {
Input<?> input = parseGame();
if (!writeModule.isEmpty()) {
Map<String, String> names = writeModule.stream()
.map(s -> s.split(","))
.peek(s -> checkArgument(s.length == 2))
.collect(Collectors.toMap(s -> s[0], s -> s[1]));
if (input.game instanceof ModuleGame<?> module) {
for (Module<?> m : module.modules()) {
var output = names.get(m.agent().name());
if (output != null) {
DotWriter.writeModule(m, module, System.out);
}
}
} else {
log.log(Level.WARNING, "Modules to output specified but read an explicit game");
}
}
writeIfPresent(writeDotConcurrentGame, input.game, DotWriter::writeConcurrentGame);
var solutionList = solve(input);
try (var stream = open(writeOutput)) {
for (GameSolution<?> solution : solutionList) {
stream.println(solution.assignment().format(input.game.agents()));
}
}
return null;
}
private <S> void validate(ConcurrentGame<S> game, Collection<GameSolution<S>> solutions, Set<Map<Agent, Boolean>> validationSet) {
Set<Map<Agent, Boolean>> results = solutions.stream().map(GameSolution::assignment)
.map(p -> game.agents().stream().collect(Collectors.toUnmodifiableMap(Function.identity(), p::isWinner)))
.collect(Collectors.toUnmodifiableSet());
if (!results.equals(validationSet)) {
var invalid = Sets.difference(results, validationSet);
var missing = Sets.difference(validationSet, results);
var agents = game.agents().stream().sorted(Comparator.comparing(Agent::name)).toList();
System.err.println("Validation failed!");
if (!invalid.isEmpty()) {
System.err.println("Invalid equilibria:");
for (Map<Agent, Boolean> map : invalid) {
System.err.println(agents.stream()
.map(a -> "%s:%s".formatted(a.name(), map.get(a)))
.collect(Collectors.joining(",", "[", "]")));
}
}
if (!missing.isEmpty()) {
System.err.println("Missing equilibria:");
for (Map<Agent, Boolean> map : results) {
System.err.println(agents.stream()
.map(a -> "%s:%s".formatted(a.name(), map.get(a)))
.collect(Collectors.joining(",", "[", "]")));
}
}
System.exit(1);
}
}
}
......@@ -6,6 +6,7 @@ import com.cges.graph.SuspectGame;
import com.cges.graph.SuspectGame.EveState;
import com.cges.model.Agent;
import com.cges.model.ConcurrentGame;
import com.cges.model.Move;
import com.cges.model.PayoffAssignment;
import com.cges.parity.OinkGameSolver;
import com.cges.parity.Player;
......@@ -58,8 +59,7 @@ public final class DeviationSolver<S> implements PunishmentStrategy<S> {
private final Map<Agent, Literal> agentLiterals;
private final List<String> atomicPropositions;
private final Set<Agent> losingAgents;
private final Map<HistoryState<S>, Boolean> isWinning;
private final Map<HistoryState<S>, PriorityState<S>> initialStates;
private final Map<HistoryState<S>, Map<Move, PriorityState<S>>> initialStates;
private final Map<PriorityState<S>, PriorityState<S>> strategy;
@Nullable
private final SuspectParityGame<S> game;
......@@ -89,7 +89,6 @@ public final class DeviationSolver<S> implements PunishmentStrategy<S> {
var goal = SimplifierRepository.SYNTACTIC_FAIRNESS.apply(eveGoal);
if (goal.formula() instanceof BooleanConstant) {
isWinning = Map.of();
strategy = Map.of();
initialStates = Map.of();
game = null;
......@@ -124,19 +123,17 @@ public final class DeviationSolver<S> implements PunishmentStrategy<S> {
}
this.strategy = Map.copyOf(strategy);
Map<HistoryState<S>, Boolean> isWinning = new HashMap<>();
Map<HistoryState<S>, PriorityState<S>> initialStates = new HashMap<>();
Map<HistoryState<S>, Map<Move, PriorityState<S>>> initialStates = new HashMap<>();
for (PriorityState<S> priorityState : solvedTopLevelEveStates) {
HistoryState<S> gameHistoryState = priorityState.eve().historyState();
boolean stateWinning = paritySolution.winner(priorityState) == Player.ODD;
isWinning.put(gameHistoryState, stateWinning);
if (stateWinning) {
initialStates.put(gameHistoryState, priorityState);
}
Map<Move, PriorityState<S>> winningInitial = new HashMap<>();
game.successors(priorityState)
.filter(s -> paritySolution.winner(s) == Player.ODD)
.forEach(winner -> winningInitial.put(winner.adam().move(), winner));
initialStates.put(gameHistoryState, Map.copyOf(winningInitial));
}
this.initialStates = Map.copyOf(initialStates);
this.isWinning = Map.copyOf(isWinning);
}
}
......@@ -145,17 +142,16 @@ public final class DeviationSolver<S> implements PunishmentStrategy<S> {
.map(a -> Conjunction.of(GOperator.of(agentLiterals.get(a)), historyState.goal(a)))).not(), atomicPropositions);
}
public boolean isWinning(HistoryState<S> historyState) {
Boolean winning = this.isWinning.get(historyState);
if (winning == null) {
public boolean isWinning(HistoryState<S> historyState, Move move) {
Map<Move, PriorityState<S>> winningMoves = this.initialStates.get(historyState);
if (winningMoves == null) {
LabelledFormula eveGoal = eveGoal(historyState);
var goal = SimplifierRepository.SYNTACTIC_FAIRNESS.apply(eveGoal).formula();
assert goal instanceof BooleanConstant;
assert computeWinning(historyState) == ((BooleanConstant) goal).value;
winning = ((BooleanConstant) goal).value;
return ((BooleanConstant) goal).value;
}
assert winning == computeWinning(historyState);
return winning;
return winningMoves.containsKey(move);
}
private boolean computeWinning(HistoryState<S> historyState) {
......@@ -164,8 +160,9 @@ public final class DeviationSolver<S> implements PunishmentStrategy<S> {
}
@Override
public PriorityState<S> initialState(HistoryState<S> state) {
return initialStates.get(state);
public Set<PriorityState<S>> initialStates(HistoryState<S> state, Move proposedMove) {
PriorityState<S> initialAdam = initialStates.get(state).get(proposedMove);
return game == null ? Set.of(initialAdam) : game.successors(initialAdam).collect(Collectors.toSet());
}
@Override
......
package com.cges.algorithm;
import com.cges.graph.HistoryGame;
import com.cges.model.Move;
import com.cges.parity.PriorityState;
import java.util.Set;
public interface PunishmentStrategy<S> {
PriorityState<S> initialState(HistoryGame.HistoryState<S> state);
Set<PriorityState<S>> initialStates(HistoryGame.HistoryState<S> state, Move proposedMove);
PriorityState<S> move(PriorityState<S> state);
......
......@@ -18,6 +18,10 @@ import java.util.Set;
import java.util.stream.Collectors;
public final class RunGraphSolver {
public enum LassoSolver {
GRAPH_SEARCH, BMC
}
private RunGraphSolver() {}
private static <S> boolean validate(EquilibriumStrategy<S> strategy, RunGraph<S> graph) {
......@@ -47,7 +51,7 @@ public final class RunGraphSolver {
return true;
}
public static <S> Optional<EquilibriumStrategy<S>> solve(RunGraph<S> graph) {
public static <S> Optional<EquilibriumStrategy<S>> solve(RunGraph<S> graph, LassoSolver solver) {
Set<RunState<S>> states = new HashSet<>(graph.initialStates());
Queue<RunState<S>> queue = new ArrayDeque<>(states);
while (!queue.isEmpty()) {
......@@ -58,7 +62,10 @@ public final class RunGraphSolver {
});
}
var path = RunGraphSccSolver.search(graph);
var path = switch (solver) {
case GRAPH_SEARCH -> RunGraphSccSolver.search(graph);
case BMC -> RunGraphBmcSolver.search(graph);
};
if (path.isEmpty()) {
return Optional.empty();
}
......
......@@ -6,6 +6,7 @@ import com.cges.graph.HistoryGame.HistoryState;
import com.cges.model.Agent;
import com.cges.model.ConcurrentGame;
import com.cges.model.PayoffAssignment;
import com.cges.model.Transition;
import com.cges.output.DotFormatted;
import java.util.BitSet;
import java.util.Comparator;
......@@ -50,7 +51,7 @@ public final class RunGraph<S> {
LabelledFormula eveGoal = SimplifierRepository.SYNTACTIC_FIXPOINT.apply(LabelledFormula.of(
Conjunction.of(Stream.concat(agents.stream().map(a -> payoffAssignment.isLoser(a) ? a.goal().not() : a.goal()),
Stream.of(suspectGame.historyGame().concurrentGame().goal().formula()))),
Stream.of(suspectGame.historyGame().concurrentGame().goal().formula()))),
concurrentGame.atomicPropositions()));
LiteralMapper.ShiftedLabelledFormula shifted = LiteralMapper.shiftLiterals(eveGoal);
var translator = LtlTranslationRepository.defaultTranslation(
......@@ -66,9 +67,10 @@ public final class RunGraph<S> {
}
public Set<RunState<S>> initialStates() {
if (deviationSolver.isWinning(historyGame.initialState())) {
HistoryState<S> initialState = historyGame.initialState();
if (historyGame.transitions(initialState).map(Transition::move).anyMatch(m -> deviationSolver.isWinning(initialState, m))) {
return automaton.initialStates().stream()
.map(s -> new RunState<>(s, historyGame.initialState()))
.map(s -> new RunState<>(s, initialState))
.collect(Collectors.toSet());
}
return Set.of();
......@@ -95,11 +97,11 @@ public final class RunGraph<S> {
return Set.of();
}
var transitions = historyGame.transitions(current.historyState())
.filter(t -> deviationSolver.isWinning(t.destination()))
.filter(t -> deviationSolver.isWinning(current.historyState(), t.move()))
.flatMap(transition -> automatonEdges.stream().map(edge ->
new RunTransition<>(new RunState<>(edge.successor(), transition.destination()), !edge.colours().isEmpty())))
.collect(Collectors.toSet());
assert !transitions.isEmpty() : "No winning successor in state %s".formatted(current);
assert !transitions.isEmpty() : "No winning moves in state %s".formatted(current);
return transitions;
}
......
......@@ -4,9 +4,7 @@ import com.cges.graph.HistoryGame.HistoryState;
import com.cges.model.Agent;
import com.cges.model.Move;
import com.cges.model.Transition;
import com.google.common.collect.HashMultimap;
import com.google.common.collect.ImmutableSetMultimap;
import com.google.common.collect.SetMultimap;
import com.google.common.collect.Sets;
import java.util.Collection;
import java.util.HashMap;
......@@ -36,29 +34,41 @@ public final class SuspectGame<S> {
return initialState;
}
public Stream<SuspectTransition<S>> transitions(EveState<S> eveState) {
return transitions.computeIfAbsent(eveState, this::computeSuccessorMap).entrySet().stream()
public Stream<SuspectTransition<S>> deviatingTransitions(EveState<S> eveState) {
return transitions.computeIfAbsent(eveState, this::computeDeviatingSuccessors).entrySet().stream()
.flatMap(entry -> entry.getValue().stream().map(eve -> new SuspectTransition<>(entry.getKey(), eve)));
}
public Stream<AdamState<S>> successors(EveState<S> eveState) {
return Set.copyOf(transitions.computeIfAbsent(eveState, this::computeSuccessorMap).keySet()).stream();
return Set.copyOf(transitions.computeIfAbsent(eveState, this::computeDeviatingSuccessors).keySet()).stream();
}
public Stream<EveState<S>> successors(AdamState<S> adamState) {
return transitions.computeIfAbsent(adamState.eveState(), this::computeSuccessorMap).get(adamState).stream();
return Stream.concat(Stream.of(compliantSuccessor(adamState)), deviationSuccessors(adamState));
}
public Stream<EveState<S>> deviationSuccessors(AdamState<S> adamState) {
return transitions.computeIfAbsent(adamState.eveState(), this::computeDeviatingSuccessors).get(adamState).stream();
}
public EveState<S> compliantSuccessor(AdamState<S> adamState) {
EveState<S> eve = adamState.eveState();
return new EveState<>(game.transition(eve.historyState(), adamState.move()).orElseThrow().destination(), eve.suspects());
}
public Stream<EveState<S>> eveSuccessors(EveState<S> eveState) {
return transitions.computeIfAbsent(eveState, this::computeSuccessorMap).values().stream().flatMap(Collection::stream);
return Stream.concat(transitions.computeIfAbsent(eveState, this::computeDeviatingSuccessors)
.values().stream().flatMap(Collection::stream),
game.transitions(eveState.historyState()).map(t -> new EveState<S>(t.destination(), eveState.suspects())));
}
private Map<AdamState<S>, Set<EveState<S>>> computeSuccessorMap(EveState<S> eveState) {
private Map<AdamState<S>, Set<EveState<S>>> computeDeviatingSuccessors(EveState<S> eveState) {
var gameState = eveState.historyState();
// Can only happen if we start with an empty set of suspects (= all agents winning)
if (eveState.suspects().isEmpty()) {
return game.transitions(gameState).collect(Collectors.toUnmodifiableMap(
t -> new AdamState<>(eveState, Set.of(t.move())),
t -> new AdamState<>(eveState, t.move()),
t -> Set.of(new EveState<>(t.destination(), Set.of()))
));
}
......@@ -72,8 +82,7 @@ public final class SuspectGame<S> {
// TODO This is somewhat inefficient, but smaller than the subsequent algorithmic problems
// Which set of states can we reach by deviating from the move
SetMultimap<Set<EveState<S>>, Move> reachableByDeviation = HashMultimap.create();
Map<AdamState<S>, Set<EveState<S>>> transitions = new HashMap<>();
game.transitions(gameState).forEach(proposedTransition -> {
// Eve proposes this transition -- adam can either comply or change the choice of one suspect
Move proposedMove = proposedTransition.move();
......@@ -87,12 +96,8 @@ public final class SuspectGame<S> {
Set<Move> movesLeadingToAlternative = movesToSuccessors.get(alternativeSuccessor);
assert !alternativeSuccessor.equals(proposedTransition.destination()) || movesLeadingToAlternative.contains(proposedMove);
Collection<Agent> successorSuspects;
if (movesLeadingToAlternative.contains(proposedMove)) {
// Nobody needs to deviate to achieve this move
successorSuspects = currentSuspects;
} else {
successorSuspects = new HashSet<>();
if (!movesLeadingToAlternative.contains(proposedMove)) {
Collection<Agent> successorSuspects = new HashSet<>();
for (Move move : movesLeadingToAlternative) {
if (nonSuspects.stream().allMatch(a -> move.action(a).equals(proposedMove.action(a)))) {
// Check if there is a single suspect who could deviate to achieve this move (i.e. move to the successor)
......@@ -111,30 +116,20 @@ public final class SuspectGame<S> {
== (deviating != null);
}
}
}
assert currentSuspects.containsAll(successorSuspects);
if (!successorSuspects.isEmpty()) {
deviationSuccessors.add(new EveState<>(alternativeSuccessor, Set.copyOf(successorSuspects)));
assert currentSuspects.containsAll(successorSuspects);
if (!successorSuspects.isEmpty()) {
deviationSuccessors.add(new EveState<>(alternativeSuccessor, Set.copyOf(successorSuspects)));
}
}
});
assert deviationSuccessors.stream().map(EveState::historyState).anyMatch(proposedTransition.destination()::equals);
reachableByDeviation.put(Set.copyOf(deviationSuccessors), proposedTransition.move());
transitions.put(new AdamState<>(eveState, proposedMove), Set.copyOf(deviationSuccessors));
});