/*
$Id$
Copyright (C) 2006-2007 by David Cotton
This program 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 2 of the License, or (at your option) any later
version.
This program 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
this program; if not, write to the Free Software Foundation, Inc., 51 Franklin
Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
package fr.free.jchecs.ai;
import static fr.free.jchecs.core.BoardFactory.State.EMPTY;
import static fr.free.jchecs.core.BoardFactory.Type.FASTEST;
import java.io.DataInputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InputStream;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.regex.Pattern;
import java.util.zip.GZIPInputStream;
import fr.free.jchecs.core.BoardFactory;
import fr.free.jchecs.core.Move;
import fr.free.jchecs.core.MoveGenerator;
/**
* Implémentation de base des moteurs d'IA pour les échecs.
*
* @author David Cotton
*/
abstract class AbstractEngine implements Engine
{
/** Générateur de nombres aléatoires. */
protected static final Random RANDOMIZER = new Random();
/** Valeur d'un Mat. */
protected static final int MATE_VALUE = Integer.MIN_VALUE / 2;
/** Modèle de découpage des enregistrements des ouvertures suivant les ';'. */
static final Pattern SPLITTER = Pattern.compile(";");
/** Buffer des ouvertures. */
private static Map<Integer, int []> S_openings;
static
{
final Thread preload = new Thread(new Runnable()
{
/**
* Tâche de fond pour masquer le temps de chargement...
*/
public void run()
{
getFromOpenings(BoardFactory.valueOf(FASTEST, EMPTY));
}
});
preload.setPriority(Thread.MIN_PRIORITY);
preload.start();
}
/** Limite basse de la profondeur de recherche. */
private final int _minimalSearchDepth;
/** Limite haute de la profondeur de recherche. */
private final int _maximalSearchDepth;
/** Temps total passé en traitement par le moteur. */
private long _elapsedTime;
/** Nombre total de demi-coups évalué par le moteur. */
private int _halfmoveCount;
/** Fonction d'évalutation utilisée par le moteur. */
private Heuristic _heuristic;
/** Fonction de tri des mouvements. */
private Comparator<Move> _moveSorter;
/** Drapeau signalant l'activation de la bibliothèque d'ouvertures. */
private boolean _openingsEnabled;
/** Score du dernier mouvement. */
private int _score;
/** Limite de la profondeur de recherche (en demi-coups). */
private int _searchDepthLimit;
/**
* Instancie un nouveau moteur IA.
*
* @param pProfMin Limite basse de la profondeur de recherche (>= 1).
* @param pProfMax Limite haute de la profondeur de recherche (>= pProfMin).
* @param pProfDef Limite par défaut de la profondeur de recherche ([pProfMin, pProfMax]).
*/
protected AbstractEngine(final int pProfMin, final int pProfMax, final int pProfDef)
{
assert pProfMin >= 1;
assert pProfMax >= pProfMin;
_minimalSearchDepth = pProfMin;
_maximalSearchDepth = pProfMax;
setSearchDepthLimit(pProfDef);
setHeuristic(new MobilityHeuristic());
setMoveSorter(new StaticMoveSorter());
setOpeningsEnabled(true);
}
/**
* Ajoute une durée (en ms) au temps total de traitement par le moteur.
*
* @param pDuree Duree en ms à ajouter.
*/
protected final void addElapsedTime(final long pDuree)
{
assert pDuree >= 0;
_elapsedTime += pDuree;
}
/**
* Ajoute un décompte de demi-coups au nombre de demi-coups évalués par le moteur.
*
* @param pNombre Nombre de demi-coups à ajouter.
*/
protected final void addHalfmove(final int pNombre)
{
assert pNombre >= 0;
_halfmoveCount += pNombre;
}
/**
* Renvoi le temps total passé en traitement par le moteur.
*
* @return Temps total de traitement par le moteur (en ms).
*/
public final long getElapsedTime()
{
assert _elapsedTime >= 0;
return _elapsedTime;
}
/**
* Renvoi le mouvement correspondant à une position dans la bibliothèque d'ouverture.
*
* @param pEtat Etat du jeu.
* @return Mouvement correspondant (ou null)
*/
static final synchronized Move getFromOpenings(final MoveGenerator pEtat)
{
assert pEtat != null;
Move res = null;
if (S_openings == null)
{
final InputStream is = Engine.class.getResourceAsStream("jchecs.opn");
if (is != null)
{
S_openings = new HashMap<Integer, int []>();
DataInputStream in = null;
try
{
in = new DataInputStream(new GZIPInputStream(is));
while (in.available() > 0)
{
final int nb = in.readByte();
assert (nb > 0) && (nb <= 5);
final int [] mvtsId = new int [ nb ];
final int cle = in.readInt();
for (int i = 0; i < nb; i++)
{
mvtsId[i] = (in.readUnsignedShort() << 8) + in.readUnsignedByte();
assert (mvtsId[i] & 0xFF000000) == 0;
}
S_openings.put(Integer.valueOf(cle), mvtsId);
}
}
catch (final EOFException e)
{
// Pas grave, le coup sera calculé...
}
catch (final IOException e)
{
// Pas grave, le coup sera calculé...
assert false;
}
finally
{
if (in != null)
{
try
{
in.close();
}
catch (final IOException e1)
{
// On aura essayé :-)
}
}
}
}
}
int [] ids = null;
if (S_openings != null)
{
ids = S_openings.get(Integer.valueOf(pEtat.hashCode()));
}
if (ids != null)
{
res = Move.valueOf(ids[RANDOMIZER.nextInt(ids.length)]);
// Les hashcodes n'étant pas infaïbles, il vaut mieux valider le mouvement obtenu...
boolean erreurHashcode = true;
for (final Move mvt : pEtat.getValidMoves(pEtat.isWhiteActive()))
{
if (mvt.equals(res))
{
erreurHashcode = false;
break;
}
}
if (erreurHashcode)
{
res = null;
}
}
return res;
}
/**
* Renvoi le nombre total de demi-coups évalués par le moteur.
*
* @return Nombre total de demi-coups évalués par le moteur.
*/
public final int getHalfmoveCount()
{
assert _halfmoveCount >= 0;
return _halfmoveCount;
}
/**
* Renvoi la fonction d'évaluation utilisée par le moteur.
*
* @return Fonction d'évaluation utilisée.
*/
public final Heuristic getHeuristic()
{
assert _heuristic != null;
return _heuristic;
}
/**
* Renvoi la limite haute de la profondeur de recherche supportées par le moteur.
*
* @return Limite haute de la profondeur de recherche (>= getMinimalSearchDepth()).
*/
public final int getMaximalSearchDepth()
{
assert _maximalSearchDepth >= _searchDepthLimit;
return _maximalSearchDepth;
}
/**
* Renvoi la limite basse de la profondeur de recherche supportées par le moteur.
*
* @return Limite basse de la profondeur de recherche (>= 1).
*/
public final int getMinimalSearchDepth()
{
assert _minimalSearchDepth <= _searchDepthLimit;
return _minimalSearchDepth;
}
/**
* Recherche un mouvement répondant à un état de l'échiquier.
*
* @param pEtat Etat de l'échiquier.
* @return Mouvement trouvé.
*/
public final synchronized Move getMoveFor(final MoveGenerator pEtat)
{
assert pEtat != null;
final long debut = System.currentTimeMillis();
Move res = null;
setScore(0);
if (_openingsEnabled && (pEtat.getFullmoveNumber() < 20))
{
res = getFromOpenings(pEtat);
}
if (res == null)
{
// Calcul du meilleur coup...
final Move [] coups = pEtat.getValidMoves(pEtat.isWhiteActive());
assert coups.length > 0;
res = searchMoveFor(pEtat, coups);
}
final long duree = System.currentTimeMillis() - debut;
addElapsedTime(duree);
assert res != null;
return res;
}
/**
* Renvoi la fonction de tri des mouvements.
*
* @return Fonction de tri des mouvements.
*/
public final Comparator<Move> getMoveSorter()
{
assert _moveSorter != null;
return _moveSorter;
}
/**
* Renvoi le score obtenu par le dernier mouvement calculé.
*
* @return Score du dernier mouvement.
*/
public final int getScore()
{
return _score;
}
/**
* Renvoi la valeur limite de la profondeur de recherche (en demi-coups).
*
* @return Limite de la profondeur de recherche ([getMinimalSearchDepth(),
* getMaximalSearchDepth()]).
*/
public final int getSearchDepthLimit()
{
assert _searchDepthLimit >= _minimalSearchDepth;
return _searchDepthLimit;
}
/**
* Indique si l'utilisation de la bibliothèque d'ouvertures est activée.
*
* @return "true" si les ouvertures sont utilisées, "false" sinon.
*/
public final boolean isOpeningsEnabled()
{
return _openingsEnabled;
}
/**
* Corps de la recherche du "meilleur" demi-coup pour un état de l'échiquier.
*
* @param pEtat Etat de l'échiquier.
* @param pCoups Liste des mouvement initiaux valides.
* @return Mouvement trouvé.
*/
protected abstract Move searchMoveFor(final MoveGenerator pEtat, final Move [] pCoups);
/**
* Modifie la fonction d'évaluation utilisée par le moteur.
*
* @param pHeuristique Nouvelle fonction d'évaluation à utiliser.
*/
public final void setHeuristic(final Heuristic pHeuristique)
{
assert pHeuristique != null;
_heuristic = pHeuristique;
}
/**
* Modifie la fonction d'ordenancement des mouvements.
*
* @param pComparateur Nouvelle fonction de tri des mouvements.
*/
public final void setMoveSorter(final Comparator<Move> pComparateur)
{
assert pComparateur != null;
_moveSorter = pComparateur;
}
/**
* Active / désactive l'utilisation de la bibliothèque d'ouvertures.
*
* @param pActif A "true" pour activer l'utilisation des ouvertures, à "false" sinon.
*/
public final void setOpeningsEnabled(final boolean pActif)
{
_openingsEnabled = pActif;
}
/**
* Alimente le score obtenu par le dernier mouvement calculé.
*
* @param pScore Score du dernier mouvement.
*/
protected final void setScore(final int pScore)
{
_score = pScore;
}
/**
* Aliment la valeur de la limite de la profondeur de recherche (en demi-coups).
*
* @param pLimite Limite de la profondeur de recherche ([getMinimalSearchDepth(),
* getMaximalSearchDepth()]).
*/
public final void setSearchDepthLimit(final int pLimite)
{
assert pLimite >= _minimalSearchDepth;
_searchDepthLimit = pLimite;
}
}