org.ggp.base.apps.benchmark.EndgameCaseGenerator Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of alloy-ggp-base Show documentation
Show all versions of alloy-ggp-base Show documentation
A modified version of the GGP-Base library for Alloy.
The newest version!
package org.ggp.base.apps.benchmark;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.ggp.base.util.Pair;
import org.ggp.base.util.game.Game;
import org.ggp.base.util.game.GameRepository;
import org.ggp.base.util.gdl.grammar.GdlSentence;
import org.ggp.base.util.statemachine.MachineState;
import org.ggp.base.util.statemachine.Move;
import org.ggp.base.util.statemachine.Role;
import org.ggp.base.util.statemachine.StateMachine;
import org.ggp.base.util.statemachine.exceptions.GoalDefinitionException;
import org.ggp.base.util.statemachine.exceptions.MoveDefinitionException;
import org.ggp.base.util.statemachine.exceptions.TransitionDefinitionException;
import org.ggp.base.util.statemachine.implementation.prover.ProverStateMachine;
/**
* EndgameCaseGenerator uses a minimax solver to produce some test cases
* for the PlayerTester. It runs a depth charge from the initial state of
* a game down to a terminal state, and then backs off and runs minimax to
* find the best moves in the resulting state. Since the state is near the
* end of the game, the resulting minimax solve will usually finish quickly.
* This produces a nice test case that can be used in the PlayerTester.
*
* Please note this will only produce a subset of the collection of tests that
* are useful in PlayerTester: all of the tests that it produces are for states
* that are near an ending to the game. Notably, it doesn't test critical moves
* in the mid-game that heavily impact the outcome, but don't immediately lead
* to a victory or loss for one player.
*
* @author Sam Schreiber
*/
public class EndgameCaseGenerator {
private EndgameCaseGenerator() {
}
public static void main(String[] args) throws MoveDefinitionException, TransitionDefinitionException, GoalDefinitionException {
generateTestCase("connectFour", 0, 5, 6, new ProverStateMachine());
}
public static void generateTestCase(String gameKey, int nRole, int nBackoff, int nMaxDepth, StateMachine theMachine) throws MoveDefinitionException, TransitionDefinitionException, GoalDefinitionException {
// Load the game and create a state machine for it
Game theGame = GameRepository.getDefaultRepository().getGame(gameKey);
theMachine.initialize(theGame.getRules());
Role ourRole = theMachine.getRoles().get(nRole);
// Once the game is loaded, run depth charges until we find a suitable
// endgame backoff state that can be used to produce a test case.
while (true) {
// Find a state that's nBackoff steps from a terminal state
List theStates = new ArrayList();
MachineState theChargeState = theMachine.getInitialState();
while (!theMachine.isTerminal(theChargeState)) {
theStates.add(theChargeState);
theChargeState = theMachine.getRandomNextState(theChargeState);
}
MachineState theState = theStates.get(Math.max(theStates.size() - nBackoff, 0));
// Solve the game from the backoff state. For moves that return
// definite scores, track the best and worst scores. For moves that
// don't return definite scores, track them separately.
int bestScore = 0;
int worstScore = 100;
List> scoredMoves = new ArrayList>();
Set unscoredMoves = new HashSet();
for (Move ourMove : theMachine.getLegalMoves(theState, ourRole)) {
Pair theScore = minimax(theMachine, nRole, ourRole, theMachine.getRandomNextState(theState, ourRole, ourMove), nMaxDepth);
if (theScore.left == theScore.right) {
bestScore = Math.max(bestScore, theScore.left);
worstScore = Math.min(worstScore, theScore.left);
scoredMoves.add(Pair.of(ourMove, theScore.left));
} else {
unscoredMoves.add(ourMove);
}
}
// If no moves had definite scores, it's a bad test case. Repeat!
if (worstScore > bestScore) {
System.out.println("Found a backoff state with no fully scored moves.");
continue;
}
// If all scored moves are equally good, it's a bad test case. Repeat!
if (bestScore == worstScore) {
System.out.println("Found a backoff state in which all scored moves were equal at " + bestScore);
continue;
}
// Select out the best moves for the "known good" answer. Also
// include any unscored moves in this set, since they might be
// good choices as well (we just don't know).
Set bestMoves = new HashSet();
for (Pair scoredMove : scoredMoves) {
if (scoredMove.right == bestScore) {
bestMoves.add(scoredMove.left);
}
}
StringBuilder goodMoveStrings = new StringBuilder();
for (Move bestMove : bestMoves) {
goodMoveStrings.append("\"" + bestMove + "\", ");
}
for (Move unscoredMove : unscoredMoves) {
goodMoveStrings.append("\"" + unscoredMove + "\", ");
}
// Also represent the current state as a symbol list.
StringBuilder theStateSymbols = new StringBuilder("( ");
for (GdlSentence aSentence : theState.getContents()) {
theStateSymbols.append(aSentence.getBody().get(0) + " ");
}
theStateSymbols.append(")");
// And finally, output the code for the new test.
System.out.println("new PlayerTester.TestCase(\"Endgame\", \"" + gameKey + "\", 0, 15, 5, \"" + theStateSymbols + "\", new String[] {" + goodMoveStrings + "}),");
return;
}
}
// This is a traditional minimax solver.
private static Pair minimax(StateMachine machine, int ourRoleIndex, Role ourRole, MachineState currentState, int depth) throws GoalDefinitionException, MoveDefinitionException, TransitionDefinitionException {
// If we've hit our max depth, return immediately, sending back
// no useful information about the final score.
if (depth < 0) {
return Pair.of(0, 100);
}
// Upon reaching a terminal state, we know its value immediately.
if (machine.isTerminal(currentState)) {
int theScore = machine.getGoal(currentState, ourRole);
return Pair.of(theScore, theScore);
}
// Otherwise, perform recursive descent to compute the state's value.
List> legalMoves = machine.getLegalJointMoves(currentState);
Pair overallScore = Pair.of(0, 0);
for (Move ourMove : machine.getLegalMoves(currentState, ourRole)) {
Pair worstMove = Pair.of(100, 100);
for (List jointMove : legalMoves) {
if (jointMove.get(ourRoleIndex).equals(ourMove)) {
MachineState newState = machine.getNextState(currentState, jointMove);
Pair score = minimax(machine, ourRoleIndex, ourRole, newState, depth - 1);
worstMove = Pair.of(Math.min(worstMove.left, score.left), Math.min(worstMove.right, score.right));
if(score.right == 0)
break;
}
}
overallScore = Pair.of(Math.max(overallScore.left, worstMove.left), Math.max(overallScore.right, worstMove.right));
if(overallScore.left == 100)
break;
}
return overallScore;
}
}