/*
$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.core;
import static fr.free.jchecs.core.Constants.FILE_COUNT;
import static fr.free.jchecs.core.Constants.RANK_COUNT;
/**
* Classes fournissant des fonctions utilitaires pour gérer la notation FEN.
* <p>
* Classe sûre vis-à-vis des threads.
* </p>
*
* @author David Cotton
*/
public final class FENUtils
{
/** Chaine FEN de la position de départ. */
public static final String STANDART_STARTING_FEN =
"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
/**
* Classe utilitaire : ne pas instancier.
*/
private FENUtils()
{
// Rien de spécifique...
}
/**
* Evalue le champ de notation du trait d'une chaine FEN.
*
* @param pChamp Contenu du champ FEN du trait.
* @param pEtat Etat du jeu à paramètrer en fonction.
* @throws FENException en cas d'erreur dans le champ.
*/
private static void parseFENActiveColor(final String pChamp, final MutableBoard pEtat)
throws FENException
{
assert pChamp != null;
assert pEtat != null;
if ((pChamp.length() != 1) || ("bw".indexOf(pChamp.charAt(0)) < 0))
{
throw new FENException("Invalid FEN active color [" + pChamp + ']', null);
}
pEtat.setWhiteActive(pChamp.charAt(0) == 'w');
}
/**
* Evalue le champ de notation des possiblités de roque d'une chaine FEN.
*
* @param pChamp Contenu du champ FEN du roque.
* @param pEtat Etat du jeu à paramètrer en fonction.
* @throws FENException en cas d'erreur dans le champ.
*/
private static void parseFENCastling(final String pChamp, final MutableBoard pEtat)
throws FENException
{
assert pChamp != null;
assert pEtat != null;
final int l = pChamp.length();
if ((l < 1) || (l > 4))
{
throw new FENException("Invalid FEN castling field [" + pChamp + ']', null);
}
pEtat.setCastleLong(false, false);
pEtat.setCastleLong(true, false);
pEtat.setCastleShort(false, false);
pEtat.setCastleShort(true, false);
if ((l == 1) && (pChamp.charAt(0) == '-'))
{
return;
}
for (int i = l - 1; i >= 0; i--)
{
switch (pChamp.charAt(i))
{
case 'k' :
pEtat.setCastleShort(false, true);
break;
case 'K' :
pEtat.setCastleShort(true, true);
break;
case 'q' :
pEtat.setCastleLong(false, true);
break;
case 'Q' :
pEtat.setCastleLong(true, true);
break;
default :
throw new FENException("Invalid FEN castling field [" + pChamp + ']', null);
}
}
}
/**
* Evalue le champ "En passant" d'une chaine FEN.
*
* @param pChamp Contenu du champ FEN "En passant".
* @param pEtat Etat du jeu à paramètrer en fonction.
* @throws FENException en cas d'erreur dans le champ.
*/
private static void parseFENEnPassant(final String pChamp, final MutableBoard pEtat)
throws FENException
{
assert pChamp != null;
assert pEtat != null;
final int l = pChamp.length();
if ((l < 1) || (l > 2))
{
throw new FENException("Invalid FEN 'en passant' field [" + pChamp + ']', null);
}
if ((l != 1) || (pChamp.charAt(0) != '-'))
{
try
{
pEtat.setEnPassant(Square.valueOf(pChamp));
}
catch (final IllegalArgumentException e)
{
throw new FENException("Invalid FEN 'en passant' field [" + pChamp + ']', e);
}
}
}
/**
* Evalue le champ "numéro de coup" d'une chaine FEN.
*
* @param pChamp Contenu du champ FEN stockant le numéro du coup.
* @param pEtat Etat du jeu à paramètrer en fonction.
* @throws FENException en cas d'erreur dans le champ.
*/
private static void parseFENFullmove(final String pChamp, final MutableBoard pEtat)
throws FENException
{
assert pChamp != null;
assert pEtat != null;
try
{
final int num = Integer.parseInt(pChamp);
if (num <= 0)
{
throw new FENException("Invalid FEN fullmove number field [" + pChamp + ']', null);
}
pEtat.setFullmoveNumber(num);
}
catch (final NumberFormatException e)
{
throw new FENException("Invalid FEN fullmove number field [" + pChamp + ']', e);
}
}
/**
* Evalue le champ "nombre de demi-coups" d'une chaine FEN.
*
* @param pChamp Contenu du champ FEN stockant le nombre de demi-coups depuis la dernière prise ou
* mouvement de pion.
* @param pEtat Etat du jeu à paramètrer en fonction.
* @throws FENException en cas d'erreur dans le champ.
*/
private static void parseFENHalfmove(final String pChamp, final MutableBoard pEtat)
throws FENException
{
assert pChamp != null;
assert pEtat != null;
try
{
final int nb = Integer.parseInt(pChamp);
if ((nb < 0) || (pChamp.length() == 0))
{
throw new FENException("Invalid FEN halfmove clock field [" + pChamp + ']', null);
}
pEtat.setHalfmoveCount(nb);
}
catch (final NumberFormatException e)
{
throw new FENException("Invalid FEN halfmove clock field [" + pChamp + ']', e);
}
}
/**
* Evalue le champ de positionnement des pièces d'une chaine FEN.
*
* @param pChamp Contenu du champ FEN de positionnement des pièces.
* @param pEtat Etat du jeu à paramètrer en fonction.
* @throws FENException en cas d'erreur dans le champ.
*/
private static void parseFENPlacement(final String pChamp, final MutableBoard pEtat)
throws FENException
{
assert pChamp != null;
assert pEtat != null;
int rang = RANK_COUNT - 1;
int col = 0;
for (int i = 0; i < pChamp.length(); i++)
{
final char c = pChamp.charAt(i);
if (c == '/')
{
if ((col != FILE_COUNT) || (rang <= 0))
{
throw new FENException("Invalid piece placement field [" + pChamp + ']', null);
}
rang--;
col = 0;
}
else if ("12345678".indexOf(c) >= 0)
{
final int rep = c - '0';
for (int j = 0; j < rep; j++)
{
try
{
pEtat.setPieceAt(null, Square.valueOf(col++, rang));
}
catch (final IllegalArgumentException e)
{
throw new FENException("Invalid piece placement field [" + pChamp + ']', e);
}
}
}
else
{
final Piece p = Piece.valueOf(c);
if (p == null)
{
throw new FENException("Invalid piece placement field [" + pChamp + ']', null);
}
try
{
pEtat.setPieceAt(p, Square.valueOf(col, rang));
}
catch (final IllegalArgumentException e)
{
throw new FENException("Invalid piece placement field [" + pChamp + ']', e);
}
col++;
}
if (col > FILE_COUNT)
{
throw new FENException("Invalid piece placement field [" + pChamp + ']', null);
}
}
if ((col != FILE_COUNT) || (rang != 0))
{
throw new FENException("Invalid piece placement field [" + pChamp + ']', null);
}
}
/**
* Renvoi la description d'état de jeu correspondant à une chaine FEN particulière.
*
* @param pFEN Chaine FEN décrivant un état.
* @return Instance correspondante de description d'état du jeu.
* @throws FENException en cas d'erreur dans le format de la chaine FEN.
*/
public static Board toBoard(final String pFEN) throws FENException
{
if (pFEN == null)
{
throw new NullPointerException("Missing FEN string");
}
final String [] fields = pFEN.split(" ");
if (fields.length != 6)
{
throw new FENException("Invalid FEN string [" + pFEN + ']', null);
}
final MutableBoard res = new MutableBoard();
parseFENPlacement(fields[0], res);
parseFENActiveColor(fields[1], res);
parseFENCastling(fields[2], res);
parseFENEnPassant(fields[3], res);
parseFENHalfmove(fields[4], res);
parseFENFullmove(fields[5], res);
return res;
}
/**
* Renvoi la chaine FEN correspondant à un état du jeu.
*
* @param pEtat Etat du jeu.
* @return Chaine FEN décrivant l'état.
*/
public static String toFEN(final Board pEtat)
{
if (pEtat == null)
{
throw new NullPointerException("Missing game state");
}
final StringBuilder res = new StringBuilder(toFENKey(pEtat));
res.append(' ');
// Champ du compteur de demi-coups...
res.append(Integer.toString(pEtat.getHalfmoveCount()));
res.append(' ');
// Champ du numéro de coup...
res.append(Integer.toString(pEtat.getFullmoveNumber()));
return res.toString();
}
/**
* Renvoi le début de la chaine FEN (4 premiers champs), utilisable comme clé identifiant un etat
* (sans tenir compte du numéro du coup et de la règle des 50 demi-coups).
*
* @param pEtat Etat du jeu.
* @return Chaine FEN décrivant l'état.
*/
public static String toFENKey(final Board pEtat)
{
if (pEtat == null)
{
throw new NullPointerException("Missing game state");
}
final StringBuilder res = new StringBuilder();
// Champ des positions...
for (int y = RANK_COUNT - 1; y >= 0; y--)
{
int vide = 0;
for (int x = 0; x < FILE_COUNT; x++)
{
final Piece p = pEtat.getPieceAt(Square.valueOf(x, y));
if (p == null)
{
vide++;
}
else
{
if (vide > 0)
{
res.append((char) ('0' + vide));
vide = 0;
}
res.append(p.getFENLetter());
}
}
if (vide > 0)
{
res.append((char) ('0' + vide));
}
if (y != 0)
{
res.append('/');
}
}
res.append(' ');
// Champ du trait
if (pEtat.isWhiteActive())
{
res.append('w');
}
else
{
res.append('b');
}
res.append(' ');
// Champ des roques...
boolean roque = false;
if (pEtat.canCastleShort(true))
{
res.append('K');
roque = true;
}
if (pEtat.canCastleLong(true))
{
res.append('Q');
roque = true;
}
if (pEtat.canCastleShort(false))
{
res.append('k');
roque = true;
}
if (pEtat.canCastleLong(false))
{
res.append('q');
roque = true;
}
if (!roque)
{
res.append('-');
}
res.append(' ');
// Champ de la prise ne passant...
final Square enPassant = pEtat.getEnPassant();
if (enPassant != null)
{
res.append(enPassant.getFENString());
}
else
{
res.append('-');
}
return res.toString();
}
}