///////////////////////////////////////////////////////////////////////
// STANFORD LOGIC GROUP //
// General Game Playing Project //
// //
// Sample Player Implementation //
// //
// (c) 2007. See LICENSE and CONTRIBUTORS. //
///////////////////////////////////////////////////////////////////////
/**
*
*/
package stanfordlogic.jocular.game;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Timer;
import java.util.logging.Level;
import java.util.logging.Logger;
import stanfordlogic.game.Gamer;
import stanfordlogic.gdl.Parser;
import stanfordlogic.knowledge.BasicKB;
import stanfordlogic.knowledge.KnowledgeBase;
import stanfordlogic.prover.GroundFact;
import stanfordlogic.prover.ProofContext;
import stanfordlogic.prover.Term;
import stanfordlogic.prover.TermObject;
import stanfordlogic.util.FactCombinationIterator;
import stanfordlogic.util.Pair;
import stanfordlogic.util.TimedTaskMonitor;
import stanfordlogic.util.Triple;
import stanfordlogic.util.Util;
/**
*
*/
public class MinimaxGamer extends Gamer
{
private static final Logger logger_ = Logger.getLogger("stanfordlogic.game");
private static final Logger searchLogger_ = Logger.getLogger("stanfordlogic.game.search");
private final Timer searchTimer_;
private boolean stopSearch_;
/** The best move so far at the root search node. */
private Term bestMoveSoFar_;
/** The best score so far at the root search node. */
private int bestScoreSoFar_;
private class TimeoutException extends Exception
{
public TimeoutException()
{
super();
}
public TimeoutException(String msg)
{
super(msg);
}
}
public MinimaxGamer(String gameId, Parser p)
{
super(gameId, p);
random_ = new Random();
searchTimer_ = new Timer();
}
@Override
protected Triple<Term, String, String> moveThink()
{
searchTimer_.purge();
stopSearch_ = false;
bestMoveSoFar_ = null;
bestScoreSoFar_ = Integer.MIN_VALUE;
// Set up the play clock:
TimedTaskMonitor searchTask = new TimedTaskMonitor(this);
long time = (playClock_-2) * 1000; // convert to ms, giving ourselves a 2 second margin
searchTimer_.schedule(searchTask, time);
// We now have playClock-2 seconds to finish our search.
Pair<Term, Integer> searchResult;
// Construct the root search state
KnowledgeBase rootState = new BasicKB();
for (GroundFact ground : currentState_.getIterable()) {
rootState.setTrue(ground);
}
// ... and the proof contex to go with it
ProofContext rootContext = new ProofContext(rootState, parser_);
// Run the minimax search.
try {
searchResult = minimaxSearch(rootState, rootContext, 0);
}
catch (TimeoutException e) {
// We timed out -- default to whatever was registered as the best move so far
logger_.fine(gameId_ + ": Search clock expired, using fallback move.");
searchResult = new Pair<Term, Integer>(bestMoveSoFar_, -1);
}
if (searchResult == null || searchResult.first == null) {
// This is really really bad.
logger_.severe(gameId_ + ": No move returned by minimax search");
return null;
}
Term action = searchResult.first;
String explanation = getExplanation(searchResult);
String taunt = getTaunt(searchResult.second);
return new Triple<Term, String, String>(action, explanation, taunt);
}
private String getExplanation(Pair<Term, Integer> searchResult)
{
if (searchResult.second >= 0)
return "Minimax score is " + searchResult.second;
else
return "Timed out; using fallback move.";
}
private String getTaunt(int score)
{
if (score == 100) {
return "HAHA! I win!";
}
else if (score >= 75) {
return "I'm in pretty good shape...";
}
else if (score >= 50) {
return "Well, could be worse.";
}
else if (score > 0) {
return "Not so good for me.";
}
else if (score == 0) {
return "Well, darn.";
}
else {
return "I have no idea, really.";
}
}
private Pair<Term, Integer> minimaxSearch(KnowledgeBase state, ProofContext context,
int currentDepth) throws TimeoutException
{
newLevel(currentDepth, state);
// is this a terminal node?
GroundFact isTerminal = reasoner_.getAnAnswer(QUERY_TERMINAL, context);
if (isTerminal != null) {
return calculateTerminal(context, currentDepth);
}
// Not terminal, so do the minimax search.
// Build a list of everybody's moves.
List<List<GroundFact>> otherMoves = new ArrayList<List<GroundFact>>();
List<GroundFact> myMoves = null;
for (int i = 0; i < roles_.size(); i++)
{
TermObject role = roles_.get(i);
List<GroundFact> roleMoves = getAllAnswers(context, "legal", role.toString(), "?x");
if (roleMoves.size() == 0) {
logger_.severe(gameId_ + ": role " + role.toString() + " had no legal moves!");
}
if (i == myRoleIndex_) {
myMoves = roleMoves;
if (currentDepth == 0 && bestMoveSoFar_ == null) {
// pick a random first move if we don't have one yet
bestMoveSoFar_ = myMoves.get(random_.nextInt(myMoves.size()))
.getTerm(1);
}
}
else {
otherMoves.add(roleMoves);
}
}
// Pick my move that maximizes my score, assuming all other players
// are trying to minimize it.
Pair<Term, Integer> move =
findMaximalMove(state, context, myMoves, otherMoves, currentDepth);
return move;
}
private Pair<Term, Integer> calculateTerminal(ProofContext context, int currentDepth)
{
// figure out my score in this outcome
GroundFact myGoal = getAnAnswer(context, "goal", myRoleStr_, "?x");
int myScore = Integer.MIN_VALUE;
if (myGoal == null) {
logger_.severe(gameId_ + ": No goal for me (" + myRoleStr_ + "); using Integer.MIN_VALUE");
}
else {
try {
myScore = Integer.parseInt(myGoal.getTerm(1).toString());
}
catch (NumberFormatException e) {
logger_.severe(gameId_ + ": My goal (" + myRoleStr_ + ") was not a number; was: " + myGoal.getTerm(1));
}
}
reportTerminal(myScore, currentDepth);
return new Pair<Term,Integer>(null, myScore);
}
private Pair<Term, Integer> findMaximalMove(KnowledgeBase state,
ProofContext context,
List<GroundFact> myMoves,
List<List<GroundFact>> otherMoves,
int currentDepth) throws TimeoutException
{
Term bestMove = null;
int bestScore = Integer.MIN_VALUE;
for (GroundFact myMove: myMoves)
{
FactCombinationIterator it = new FactCombinationIterator(myMove, otherMoves, doesProcessor_);
int minScore = Integer.MAX_VALUE;
for (GroundFact [] moveSet : it)
{
// Find the combination that *minimizes* my score.
int score = getScore(state, context, moveSet, currentDepth);
if (score < minScore) {
minScore = score;
}
}
if (minScore > bestScore)
{
bestScore = minScore;
bestMove = myMove.getTerm(1);
if (currentDepth == 0 && bestScore > bestScoreSoFar_)
{
bestMoveSoFar_ = bestMove;
bestScoreSoFar_ = bestScore;
}
}
if (bestScore == 100) {
// might as well stop now!
break;
}
// is it time to stop the search?
if (stopSearch_) {
throw new TimeoutException();
}
}
if (bestMove == null || bestScore == Integer.MIN_VALUE) {
searchLogger_.severe(gameId_ + ": Failed to find best move or best score!!");
}
return new Pair<Term, Integer>(bestMove, bestScore);
}
private int getScore(KnowledgeBase state, ProofContext context, GroundFact [] moves,
int currentDepth) throws TimeoutException
{
// Create a new state, based on state and context
// First, add the moves
for (GroundFact move: moves) {
state.setTrue(move);
}
// Figure out what is true in the new state
Iterable<GroundFact> nexts = reasoner_.getAllAnswersIterable(QUERY_NEXT, context);
// FIXME: this should create a knowledge base of the same type as the current one
KnowledgeBase newState = new BasicKB();
for (GroundFact next: nexts) {
newState.setTrue(trueProcessor_.processFact(next));
}
ProofContext newContext = new ProofContext(newState, parser_);
// Run the recursive search
Pair<Term, Integer> result = minimaxSearch(newState, newContext, currentDepth+1);
// Remove the moves
for (GroundFact move: moves) {
state.setFalse(move);
}
return result.second;
}
private void newLevel(int currentDepth, KnowledgeBase state)
{
if (searchLogger_.isLoggable(Level.FINER))
{
String indent = Util.makeIndent(currentDepth);
searchLogger_.finer(indent + gameId_ + ": Entering level " + currentDepth);
String stateStr = state.stateToGdl();
searchLogger_.finer(indent + gameId_ + ": State: " + stateStr);
}
}
private void reportTerminal(int myScore, int currentDepth)
{
if (searchLogger_.isLoggable(Level.FINER))
{
String indent = Util.makeIndent(currentDepth);
searchLogger_.finer(indent + gameId_ + ": Terminal. My score: " + myScore);
}
}
@Override
public void stopIt()
{
stopSearch_ = true;
}
}