/* $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.RANK_COUNT; import static fr.free.jchecs.core.Piece.BLACK_KING; import static fr.free.jchecs.core.Piece.BLACK_PAWN; import static fr.free.jchecs.core.Piece.WHITE_KING; import static fr.free.jchecs.core.Piece.WHITE_PAWN; import static fr.free.jchecs.core.PieceType.KING; import static fr.free.jchecs.core.PieceType.PAWN; import static fr.free.jchecs.core.PieceType.QUEEN; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import java.util.regex.Pattern; /** * 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 SANUtils { /** Expression régulière permettant de valider une chaîne SAN. */ // Découpage du pattern : // Mat/Pat/Nullité : (\\+{1,2}|#|\\(=\\))? // Petit roque : 0-0<Mat/Pat/Nullité> // Grand roque : 0-0-0<Mat/Pat/Nullité> // Pion sans prise : [a-h]([1-8]|[18][BKNQR])<Mat/Pat/Nullité> // Pion avec prise : // [a-h]x[a-h]((([1-8]|[18][BKNQR])<Mat/Pat/Nullité>)|([36]<Mat/Pat/Nullité> e\\.p\\.)) // Pièces (sauf pion) : [BKNQR][a-h]?[1-8]?x?[a-h][1-8]<Mat/Pat/Nullité> public static final Pattern SAN_VALIDATOR = Pattern.compile("^(0-0(\\+{1,2}|#|\\(=\\))?)|(0-0-0(\\+{1,2}|#|\\(=\\))?)|" + "([a-h]([1-8]|[18][BKNQR])(\\+{1,2}|#|\\(=\\))?)|" + "([a-h]x[a-h]((([1-8]|[18][BKNQR])(\\+{1,2}|#|\\(=\\))?)|" + "([36](\\+{1,2}|#|\\(=\\))? e\\.p\\.)))|" + "([BKNQR][a-h]?[1-8]?x?[a-h][1-8](\\+{1,2}|#|\\(=\\))?)$"); /** * Classe utilitaire : ne pas instancier. */ private SANUtils() { // Rien de spécifique... } /** * Renvoi le mouvement correspondant à une chaine SAN appliquée à un état d'échiquier. * * @param pEtat Etat de l'échiquier. * @param pSAN Chaine SAN. * @return Mouvement correspondant. * @throws SANException en cas d'erreur dans le format de la chaine SAN. */ public static Move toMove(final MoveGenerator pEtat, final String pSAN) throws SANException { if (pEtat == null) { throw new NullPointerException("Missing game state"); } if (pSAN == null) { throw new NullPointerException("Missing SAN string"); } if (!SAN_VALIDATOR.matcher(pSAN).matches()) { throw new SANException("Invalid SAN string [" + pSAN + ']', null); } final boolean trait = pEtat.isWhiteActive(); if ("0-0".equals(pSAN)) { // Gère les petits roques... if (trait) { return new Move(WHITE_KING, Square.valueOf(4), Square.valueOf(6)); } return new Move(BLACK_KING, Square.valueOf(60), Square.valueOf(62)); } else if ("0-0-0".equals(pSAN)) { // ... les grands roques... if (trait) { return new Move(WHITE_KING, Square.valueOf(4), Square.valueOf(2)); } return new Move(BLACK_KING, Square.valueOf(60), Square.valueOf(58)); } // Gère les coups normaux... final Piece piece; int posSrc = 0; char c = pSAN.charAt(posSrc); if (Character.isLowerCase(c)) { if (trait) { piece = WHITE_PAWN; } else { piece = BLACK_PAWN; } } else { if (trait) { piece = Piece.valueOf(c); } else { piece = Piece.valueOf(Character.toLowerCase(c)); } posSrc++; } final boolean prise = pSAN.indexOf('x') >= 0; final List<Move> mvts = new ArrayList<Move>(Arrays.asList(pEtat.getValidMoves(trait))); for (int i = mvts.size() - 1; i >= 0; i--) { final Move m = mvts.get(i); final boolean capture = m.getCaptured() != null; if ((piece != m.getPiece()) || (prise != capture)) { mvts.remove(i); } } int posDst = pSAN.length() - 1; while ((posDst > 0) && (!Character.isDigit(pSAN.charAt(posDst)))) { posDst--; } final Square dst = Square.valueOf(pSAN.substring(posDst - 1, posDst + 1)); for (int i = mvts.size() - 1; i >= 0; i--) { final Move m = mvts.get(i); if (dst != m.getTo()) { mvts.remove(i); } } if (mvts.size() == 1) { return mvts.get(0); } // Supprime les ambiguités... c = pSAN.charAt(posSrc); if (Character.isLowerCase(c)) { final int col = c - 'a'; for (int i = mvts.size() - 1; i >= 0; i--) { final Move m = mvts.get(i); if (col != m.getFrom().getFile()) { mvts.remove(i); } } posSrc++; } c = pSAN.charAt(posSrc); if (Character.isDigit(c)) { final int lig = c - '1'; for (int i = mvts.size() - 1; i >= 0; i--) { final Move m = mvts.get(i); if (lig != m.getFrom().getRank()) { mvts.remove(i); } } posSrc++; } final int l = mvts.size(); if (l > 1) { throw new SANException("Ambiguous SAN string [" + pSAN + ']', null); } else if (l < 1) { throw new SANException("Illegal SAN string context [" + pSAN + ']', null); } return mvts.get(0); } /** * Renvoi la chaine SAN correspondant à un mouvement pour un état d'échiquier. * * @param pEtat Etat de l'échiquier. * @param pMouvement Mouvement à traduire. * @return Chaine SAN correspondante. */ public static String toSAN(final MoveGenerator pEtat, final Move pMouvement) { if (pEtat == null) { throw new NullPointerException("Missing game state"); } if (pMouvement == null) { throw new NullPointerException("Missing move"); } final boolean trait = pEtat.isWhiteActive(); final Piece piece = pMouvement.getPiece(); final PieceType t = piece.getType(); final StringBuilder sb = new StringBuilder(); final Square src = pMouvement.getFrom(); final Square dst = pMouvement.getTo(); final MoveGenerator apres = pEtat.derive(pMouvement, false); final int nbMvts = apres.getValidMoves(!trait).length; final int xSrc = src.getFile(); final int xDst = dst.getFile(); if ((t == KING) && (Math.abs(xSrc - xDst) > 1)) { // Roques... sb.append("0-0"); if (xSrc > xDst) { sb.append("-0"); } } else { // Normal... sb.append(t.getSANLetter()); // Recherche et levée des éventuelles ambiguités... if (t != PAWN) { final List<Move> mvts = new ArrayList<Move>(Arrays.asList(pEtat.getValidMoves(trait))); for (int i = mvts.size() - 1; i >= 0; i--) { final Move m = mvts.get(i); if ((piece != m.getPiece()) || (dst != m.getTo()) || (m.equals(pMouvement))) { mvts.remove(i); } } boolean preciser = true; for (int i = mvts.size() - 1; i >= 0; i--) { final Move m = mvts.get(i); if (xSrc != m.getFrom().getFile()) { mvts.remove(i); if (preciser) { sb.append((char) ('a' + xSrc)); preciser = false; } } } final int ySrc = src.getRank(); for (int i = mvts.size() - 1; i >= 0; i--) { final Move m = mvts.get(i); if (ySrc != m.getFrom().getRank()) { sb.append((char) ('1' + ySrc)); break; } } } if ((pEtat.getPieceAt(dst) != null) || ((dst == pEtat.getEnPassant()) && (t == PAWN))) { // Prise... if (t == PAWN) { sb.append((char) ('a' + xSrc)); } sb.append('x'); } sb.append(dst.getFENString()); if (t == PAWN) { // Cas particuliers... if (dst == pEtat.getEnPassant()) { // ... de la prise en passant... sb.append(" e.p."); } else { // ... ou de la promotion... final int yDst = dst.getRank(); if ((trait && (yDst == RANK_COUNT - 1)) || ((!trait) && (yDst == 0))) { // Le '=' n'est pas dans la version de SAN de la FIDE : sb.append('='); sb.append(QUEEN.getSANLetter()); } } } } if (apres.isInCheck(!trait)) { // Echec / Mat ... sb.append('+'); if (nbMvts == 0) { sb.append('+'); } } else if (nbMvts == 0) { // Pat ... sb.append("(=)"); } final String res = sb.toString(); assert SAN_VALIDATOR.matcher(res).matches(); return res; } }