
com.fathzer.games.ai.iterativedeepening.DeepeningPolicy Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of games-core Show documentation
Show all versions of games-core Show documentation
A core library to help implement two players games.
The newest version!
package com.fathzer.games.ai.iterativedeepening;
import java.util.ArrayDeque;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import com.fathzer.games.ai.DepthFirstSearchParameters;
import com.fathzer.games.ai.SearchResult;
import com.fathzer.games.ai.evaluation.EvaluatedMove;
import com.fathzer.games.ai.evaluation.Evaluation;
import com.fathzer.games.ai.evaluation.Evaluation.Type;
/** A policy that manages how to deepen the search.
*
Typically, it decides at which depth to start, what increment to add at each step, and if we should end prematurely.
*/
public class DeepeningPolicy extends DepthFirstSearchParameters {
private long maxTime;
private long start;
private boolean deepenOnForced;
/** Constructor.
*
By default there's no time limit to deepening and forced moves are not evaluated.
* @param maxDepth The maximum search depth.
*/
public DeepeningPolicy(int maxDepth) {
super(maxDepth);
this.maxTime = Long.MAX_VALUE;
this.start = -1;
this.deepenOnForced = false;
}
/** Start counting time.
*
This method should be called when the search starts in order to have {@link #getSpent()} and {@link #isDeepenOnForced()} works
*/
public void start() {
this.start = System.currentTimeMillis();
}
/** Gets the number of milliseconds elapsed since the call to {@link #start}
* @return a positive long.
* @throws IllegalStateException if {@link #start()} was not called
*/
public long getSpent() {
if (start<0) {
throw new IllegalStateException("Not yet started");
}
return System.currentTimeMillis()-start;
}
/** Sets the maximum time to spend in the search.
*
The default value is Long.MAX_VALUE.
* @param maxTime The number of milliseconds to spend in the search
*/
public void setMaxTime(long maxTime) {
this.maxTime = maxTime;
this.start = -1;
}
/** Gets the maximum time to spend in the search.
* @return a number of milliseconds to spend in the search
*/
public long getMaxTime() {
return maxTime;
}
/** Check whether the search should be deepened on forced moves.
* @return true it should be deepened.
* @see #setDeepenOnForced(boolean)
*/
public boolean isDeepenOnForced() {
return deepenOnForced;
}
/** Sets if the search should be deepened when only one move remains to evaluate.
*
Please note, that if this attribute is false (The default value), searching for two best moves (size=2), and
* having n possible moves but only one not yet evaluated to win or loose, will stop the evaluation
* @param deepenOnForced true to force the deepening until max depth or maxtime is reached.
*/
public void setDeepenOnForced(boolean deepenOnForced) {
this.deepenOnForced = deepenOnForced;
}
/** Gets the start depth.
* @return the start depth, default is min(2,maxDepth).
*/
public int getStartDepth() {
return Math.min(2, getDepth());
}
/** Gets next depth.
* @param currentDepth currentDepth
* @return next depth, a negative value to stop deepening. Default is currentDepth+1
*/
public int getNextDepth(int currentDepth) {
return currentDepth+1;
}
/** Guess whether there is enough time to deepen the search.
* @param depth the last finished depth
* @return true if it remains enough time. The default implementation returns true if less than half of {@link #getMaxTime()} was spent since {@link #start()} was called.
*/
public boolean isEnoughTimeToDeepen(int depth) {
return getSpent()This method is called every time a search is made except when {@link #isEnoughTimeToDeepen(int)} returns false.
* @param The type of moves
* @param currentParams The current search parameters
* @param history The search history, including the result at {@code depth}
* @param evaluations The evaluations obtained at this depth that were not removed by {@link #filterWinLooseMoves(DepthFirstSearchParameters, SearchHistory, List)}.
* @return A list of moves to deepen, an empty list to stop deepening.
*
Please note that {@code currentParams} could also be changed (typically to decrement its size when win moves are in {@code evaluations}
*
The default implementation first calls filterWinLooseMoves, then it returns an empty list if attribute is false and it remains only one move
* in {@code evaluations}
*
If not, all moves are returned.
*
By overriding this method, a custom policy can, for instance, decide that result is stable enough to stop deepening before max depth is reached,
* or stop deepening some moves that appears too bad.
*/
public List getMovesToDeepen(DepthFirstSearchParameters currentParams, SearchHistory history, List> evaluations) {
evaluations = filterWinLooseMoves(currentParams, history, evaluations);
// Stop deepening if not in deepenOnForced mode and there's only one move to deepen
return deepenOnForced || evaluations.size()>1 ? evaluations.stream().map(EvaluatedMove::getMove).toList() : Collections.emptyList();
}
/** Removes the win/loose moves from the list of moves that should be evaluated (no need to deepen such moves).
*
This method is called by {@link #getMovesToDeepen(DepthFirstSearchParameters, SearchHistory, List)}
*
Warning: Due to the use of transposition tables, this task is tricky; We can find win or loose moves at depth n with a overestimated
* number of moves to reach end of game.
* For example M+6 at depth 7 moves when only 4 are required to force mate. The problem is removing these moves from the list of moves to deepen
* can lead to wrong best moves list. For instance, if a M+4 move is first identified as M+7 at depth 7, then another move is identified at M+5.
* The first move will seems worse that the second ... and it is not true.
*
So, this default implementation will stop deepening win/loose move only if the depth matches the distance to mate.
* @param The type of moves
* @param currentParams The current search parameters
* @param history The search history, including the result at {@code depth}
* @param evaluatedMoves The evaluations obtained at {@code currentParams}'s depth
* @return The list of evaluations without the win/loose moves that we do not need to deepen.
*
If some winning moves has been removed, currentParams
is also decremented accordingly
* (for instance, if size was 3 and 2 moves are winning, currentParams
's size is set to 1,
* allowing deeper search to return only 1 move with an exact evaluation).
*/
protected List> filterWinLooseMoves(DepthFirstSearchParameters currentParams, SearchHistory history, List> evaluatedMoves) {
final AtomicInteger size = new AtomicInteger(currentParams.getSize());
final List> list = evaluatedMoves.stream().filter( em -> {
Type type = em.getEvaluation().getType();
if (type!=Type.EVAL && currentParams.getDepth()/20) {
currentParams.setSize(size.get());
return list;
} else {
return Collections.emptyList();
}
}
/** This method is called when a search is interrupted by timeout.
*
When a timeout occurs during a search at depth n, the results of the search is usually partial (some moves are missing).
*
This method allows this policy to decide if partial results should be inserted in final results or discarded. For instance, if there's no quiescence
* implemented in the search (which is usually a bad idea) merging the partial results of depth n with result of depth n-1 may lead to merge optimistic with
* pessimistic results ... which is the guarantee for ... strange results.
*
This method returns nothing but partial results can be returned in best moves passed in first argument.
*
Default implementation carefully merges the interrupted search result in the search history;
* It means it guarantees that non evaluated moves will never become better than an evaluated one.
*
The classical case is search history contains [(move x,50),(move y, 45),(move z, 44)] and interrupted search is [(move x, 40), (move y, 42)].
* Adding [(move z, 44),(move y, 42),(move x,40)] would be a very, very bad idea. In fact, the z score is probably just the result of an alpha/beta cut and may be very, very far
* from the exact move's score, making a bad move to seem better than than best moves.
*
In this case the default implementation will return [(move y,42),(move x, 40),(move z, ?<40)]
*
More precisely, if all the moves contained in history.cut() are in the interrupted search, the results are merged and
* this method ensures the score of moves absent from the interrupted search will remain out of new history.getCut().
* @param The class of moves
* @param history The search history that concern previous not interrupted searches.
* @param interruptedSearch The result of the interrupted search.
* @param interruptionDepth The depth at which interruption occurred.
* @return The search result to add in history if the merge can be done. An empty optional if the interrupted search should be discarded.
*/
public Optional> mergeInterrupted(SearchHistory history, SearchResult interruptedSearch, int interruptionDepth) {
final List> partialList = interruptedSearch.getList();
if (partialList.isEmpty() || !areMergeable(history.getAccurateMoves(), partialList)) {
return Optional.empty();
}
final List> historyMoves = history.getLastList();
final int previousLow = this.getLowerBound(historyMoves);
final boolean trap = partialList.get(partialList.size()-1).getScore()<=previousLow;
final SearchResult mergedResult = new SearchResult<>(this);
if (trap) {
// Warning, some approximatively scored moves have a better value than some of partialList
// => Replace all scores with a score lower than the lower score in partialList
final int unkownScore = partialList.get(partialList.size()-1).getEvaluation().getScore()-1;
historyMoves.stream().map(EvaluatedMove::getMove).collect(Collectors.toCollection(ArrayDeque::new)).
descendingIterator().forEachRemaining(m->mergedResult.update(m, Evaluation.score(unkownScore)));
} else {
historyMoves.forEach(em -> mergedResult.add(em.getMove(), em.getEvaluation()));
}
partialList.forEach(ev -> mergedResult.update(ev.getMove(), ev.getEvaluation()));
return Optional.of(mergedResult);
}
private boolean areMergeable(List> cut, List> partialList) {
for (EvaluatedMove cutEvalMove : cut) {
final boolean notFound = partialList.stream().filter(em->em.getMove().equals(cutEvalMove.getMove())).findAny().isEmpty();
if (notFound) {
return false;
}
}
return true;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy