/*
$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.BoardFactory.State.STARTING;
import static fr.free.jchecs.core.BoardFactory.Type.FASTEST;
import static fr.free.jchecs.core.Constants.APPLICATION_NAME;
import static fr.free.jchecs.core.Constants.APPLICATION_VERSION;
import static fr.free.jchecs.core.FENUtils.STANDART_STARTING_FEN;
import static fr.free.jchecs.core.FENUtils.toBoard;
import static fr.free.jchecs.core.SANUtils.toMove;
import java.io.BufferedReader;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.regex.Pattern;
import fr.free.jchecs.ai.EngineFactory;
/**
* Classes fournissant des fonctions utilitaires pour gérer la notation PGN.
* <p>
* Classe sûre vis-à-vis des threads.
* </p>
*
* @author David Cotton
*/
public final class PGNUtils
{
/** Modèle de découpage de chaines suivant les espaces. */
private static final Pattern SPLITTER = Pattern.compile("[ ]+");
/**
* Classe utilitaire : ne pas instancier.
*/
private PGNUtils()
{
// Rien de spécifique...
}
/**
* Formate une date suivant le standard PGN.
*
* @param pDate Date à formater.
* @return Chaine au format attendu par PGN.
*/
private static String formatPGNDate(final Date pDate)
{
assert pDate != null;
return new SimpleDateFormat("yyyy.MM.dd").format(pDate);
}
/**
* Renvoi la description de partie correspondant à la première partie rencontrée dans un flux au
* format PGN.
*
* @param pFlux Flux contenant les données au format PGN.
* @return Description de partie correspondante.
* @throws PGNException en cas d'erreur dans le format du flux PGN.
*/
public static Game toGame(final BufferedReader pFlux) throws PGNException
{
assert pFlux != null;
final Game res = new Game();
try
{
String ligneLue = pFlux.readLine();
while (ligneLue != null)
{
String ligne = ligneLue.trim();
if (ligne.startsWith("["))
{
// Interprétation des tags d'en-tête...
final int debTag = ligne.indexOf(" \"");
if ((debTag >= 0) && ligne.endsWith("\"]"))
{
final String contenu = ligne.substring(debTag + 2, ligne.indexOf("\"]")).trim();
if (ligne.startsWith("[Black \""))
{
final Player joueur = res.getPlayer(false);
if (contenu.startsWith(APPLICATION_NAME + '.'))
{
joueur.setEngine(EngineFactory.newInstance(contenu));
joueur.setName(contenu.substring(APPLICATION_NAME.length() + 1));
}
else
{
joueur.setEngine(null);
joueur.setName(contenu);
}
}
else if (ligne.startsWith("[White \""))
{
final Player joueur = res.getPlayer(true);
if (contenu.startsWith(APPLICATION_NAME + '.'))
{
joueur.setEngine(EngineFactory.newInstance(contenu));
joueur.setName(contenu.substring(APPLICATION_NAME.length() + 1));
}
else
{
joueur.setEngine(null);
joueur.setName(contenu);
}
}
else if (ligne.startsWith("[FEN \""))
{
try
{
final Board depart = toBoard(contenu);
res.resetTo(BoardFactory.valueOf(FASTEST, STARTING).derive(depart));
}
catch (final FENException e)
{
throw new PGNException("Invalid FEN tag", e);
}
}
}
}
else
{
// Concaténation de la liste des mouvements...
final StringBuilder sb = new StringBuilder();
while (ligneLue != null)
{
ligne = ligneLue.trim();
if (ligne.startsWith("[Event"))
{
break;
}
sb.append(' ').append(ligne);
ligneLue = pFlux.readLine();
}
// Nettoyage de la chaine...
int p = 0;
int prof = 0;
while (p < sb.length())
{
final char c = sb.charAt(p);
if ((c == '(') || (c == '{'))
{
// Supprime les commentaires, et les propositions de nul...
prof++;
}
if ((prof != 0) || (c == '+') || (c == '#'))
{
// Supprime les marqueurs d'échecs et de mat.
sb.deleteCharAt(p);
}
else
{
if (c == 'O')
{
// Convertir les "o" majuscules en zéro...
sb.setCharAt(p, '0');
}
p++;
}
if ((c == '}') || (c == ')'))
{
prof--;
}
}
p = 0;
while (p < sb.length())
{
final char c = sb.charAt(p);
if (c == '.')
{
// Supprime les numéros de coups...
int deb = p - 1;
while ((deb >= 0) && Character.isDigit(sb.charAt(deb)))
{
deb--;
}
int fin = p + 1;
while ((fin < sb.length()) && ((" .".indexOf(sb.charAt(fin))) >= 0))
{
fin++;
}
sb.delete(deb + 1, fin);
}
else if (c == '$')
{
// Supprime les annotations numériques...
int fin = p + 1;
while ((fin < sb.length()) && Character.isDigit(sb.charAt(fin)))
{
fin++;
}
sb.delete(p, fin + 1);
}
else
{
p++;
}
}
for (final String mvt : SPLITTER.split(sb.toString()))
{
if ("*".equals(mvt) || "1-0".equals(mvt) || "0-1".equals(mvt) || "1/2-1/2".equals(mvt))
{
break;
}
if (mvt.length() > 0)
{
try
{
res.moveFromCurrent(toMove(res.getBoard(), toNormalizedSAN(mvt)));
}
catch (final SANException e)
{
throw new PGNException("Invalid PGN stream", e);
}
}
}
break;
}
ligneLue = pFlux.readLine();
}
}
catch (final IOException e)
{
throw new PGNException("PGN stream reading error", e);
}
return res;
}
/**
* Converti une chaîne de mouvement "PGN" en chaine de mouvement "SAN" normalisé.
*
* @param pChaine Chaine de mouvement PGN.
* @return Chaine de mouvement SAN.
*/
public static String toNormalizedSAN(final String pChaine)
{
assert pChaine != null;
final StringBuilder res = new StringBuilder(pChaine);
// Le marqueur 'P' pour les pions est possible avec PGN, pas avec SAN...
if (res.charAt(0) == 'P')
{
res.deleteCharAt(0);
}
// Les promotions sont indiquées avec un '=' sous PGN, pas sous SAN...
int p = res.indexOf("=");
while (p >= 0)
{
res.deleteCharAt(p);
p = res.indexOf("=");
}
// Les suffixes d'annotations '?' et '!' ne font pas partie de SAN...
while ((res.length() > 0) && ("?!".indexOf(res.charAt(res.length() - 1)) >= 0))
{
res.deleteCharAt(res.length() - 1);
}
// Certains programmes ajoutent des '@' (totalement hors normes)...
p = res.indexOf("@");
while (p >= 0)
{
res.deleteCharAt(p);
p = res.indexOf("@");
}
return res.toString();
}
/**
* Renvoi une chaîne correspondant à l'image de la partie au format PGN.
*
* @param pPartie Description de la partie.
* @return Chaine contenant la représentation de la partie au format PGN.
*/
public static String toPGN(final Game pPartie)
{
if (pPartie == null)
{
throw new NullPointerException("Missing game description");
}
final StringBuilder sb = new StringBuilder();
sb.append("[Event \"" + APPLICATION_NAME + " v" + APPLICATION_VERSION + " chess game\"]\n");
String site = "?";
try
{
final InetAddress lh = InetAddress.getLocalHost();
site = lh.getHostName();
}
catch (final UnknownHostException e)
{
// Pas grave, on peut se passer de cette information...
}
sb.append("[Site \"").append(site).append("\"]\n");
sb.append("[Date \"").append(formatPGNDate(new Date())).append("\"]\n");
sb.append("[Round \"-\"]\n");
Player joueur = pPartie.getPlayer(true);
String nom = joueur.getName();
if (joueur.getEngine() != null)
{
nom = APPLICATION_NAME + '.' + nom;
}
sb.append("[White \"").append(nom).append("\"]\n");
joueur = pPartie.getPlayer(false);
nom = joueur.getName();
if (joueur.getEngine() != null)
{
nom = APPLICATION_NAME + '.' + nom;
}
sb.append("[Black \"").append(nom).append("\"]\n");
final String resultat;
switch (pPartie.getState())
{
case BLACK_MATES :
resultat = "0-1";
break;
case DRAWN_BY_50_MOVE_RULE :
case DRAWN_BY_TRIPLE_REPETITION :
case STALEMATE :
resultat = "1/2-1/2";
break;
case WHITE_MATES :
resultat = "1-0";
break;
case IN_PROGRESS :
resultat = "*";
break;
default :
resultat = "*";
assert false;
}
sb.append("[Result \"").append(resultat).append("\"]\n");
final String depart = pPartie.getStartingPosition();
if (!depart.equals(STANDART_STARTING_FEN))
{
sb.append("[SetUp \"1\"]\n");
sb.append("[FEN \"" + depart + "\"]\n");
}
sb.append('\n');
int col = 0;
for (final String san : pPartie.getSANStrings())
{
// PGN attend des "o" majuscules plutôts que les zéros du SAN standard...
String pgn = san.replace("0-0-0", "O-O-O");
pgn = pgn.replace("0-0", "O-O");
// ... et des "#" plutôt que "++" pour le mat...
pgn = pgn.replace("++", "#");
final int l = pgn.length();
col += l;
if (col >= 80)
{
sb.append('\n');
col = l;
}
sb.append(pgn);
}
col += resultat.length();
if (col >= 80)
{
sb.append('\n');
}
sb.append(resultat).append('\n');
return sb.toString();
}
}