package com.christophdietze.jack.shared.board; import java.util.ArrayList; import java.util.Iterator; import java.util.List; /** * SAN (Standard Algebraic Notation) * * @see {@link http://en.wikipedia.org/wiki/Pgn#Movetext} */ public class SanParser { private Position position; private int curIndex; private String sanString; private char curChar; private Move move; private PieceType promotionPiece; private int toIndex; private int fromFile; private int fromRank; private PieceType movingPiece; public SanParser() { } private void reset() { move = null; promotionPiece = null; toIndex = -1; fromFile = -1; fromRank = -1; movingPiece = PieceType.PAWN; } public Move parse(String sanString, Position position) throws SanParsingException { reset(); this.sanString = sanString; this.position = position; curIndex = sanString.length(); nextChar(); parseImpl(); if (move != null) { return move; } move = findSpecificMove(); move = new Move(move.getFrom(), move.getTo(), promotionPiece); return move; } private void parseImpl() throws SanParsingException { if (sanString.startsWith("O-O-O")) { move = position.isWhiteToMove() ? new Move(4, 2) : new Move(60, 58); return; } else if (sanString.startsWith("O-O")) { move = position.isWhiteToMove() ? new Move(4, 6) : new Move(60, 62); return; } // parse from back to front - this way we get the infos in nicer // order: 1. annotations, 2. promotional piece type, 3. destination // square, 4. source square infos, 5. moving piece type // annotations while (curChar == '+' || curChar == '#' || curChar == '!' || curChar == '?') { nextChar(); } // promotional piece type promotionPiece = PieceType.getBySymbol(curChar); if (promotionPiece != null) { nextChar(); if (curChar != '=') { throw new SanParsingException("Expected '=' before promotional piece, found " + curChar); } nextChar(); } // destination square if (curChar < '1' || curChar > '8') { throw new SanParsingException("Expected destination rank indication (1..8), found " + curChar); } int dstRank = curChar - '1'; nextChar(); if (curChar < 'a' || curChar > 'h') { throw new SanParsingException("Expected destination file indication (a..h), found " + curChar); } int dstFile = curChar - 'a'; nextChar(); toIndex = dstFile + dstRank * 8; if (!hasMoreChars()) { return; } if (curChar == 'x') { nextChar(); if (!hasMoreChars()) { return; } } // source square infos if (curChar >= '1' && curChar <= '8') { fromRank = curChar - '1'; nextChar(); if (!hasMoreChars()) { return; } } if (curChar >= 'a' && curChar <= 'h') { fromFile = curChar - 'a'; nextChar(); if (!hasMoreChars()) { return; } } // moving piece type movingPiece = PieceType.getBySymbol(curChar); if (movingPiece == null) { throw new SanParsingException("Expected moving piece type, found " + curChar); } nextChar(); if (hasMoreChars()) { throw new SanParsingException("Expected end of move, found " + curChar); } } private void nextChar() { curIndex--; if (curIndex >= 0) { curChar = sanString.charAt(curIndex); } else { curChar = '\0'; } } private boolean hasMoreChars() { return curIndex >= 0; } /** * parsing is done, now find which move is meant */ private Move findSpecificMove() throws SanParsingException { // find the pieces that are candidates List<Integer> fromIndices = position.findPieces(Piece.getFromColorAndPieceType(position.isWhiteToMove(), movingPiece)); if (fromIndices.size() == 0) { throw new SanParsingException("Found no matching piece on the board"); } if (fromIndices.size() == 1) { return new Move(fromIndices.get(0), toIndex); } List<Move> moveCandidates = new ArrayList<Move>(); for (int fromIndex : fromIndices) { moveCandidates.add(new Move(fromIndex, toIndex)); } // filter the moves that are pseudo illegal // and the ones not fitting the move disambiguating info for (Iterator<Move> iterator = moveCandidates.iterator(); iterator.hasNext();) { Move move = iterator.next(); if (fromFile >= 0 && fromFile != move.getFrom() % 8) { iterator.remove(); } else if (fromRank >= 0 && fromRank != move.getFrom() / 8) { iterator.remove(); } else if (!MoveChecker.isPseudoLegalMove(position, move).isLegal()) { iterator.remove(); } } if (moveCandidates.size() == 0) { throw new SanParsingException("None of the pieces is on the specified file and/or rank"); } if (moveCandidates.size() == 1) { return moveCandidates.get(0); } // filter the ones that cannot legally move filterIllegalMoves(moveCandidates); if (moveCandidates.size() == 0) { throw new SanParsingException("Moving any of the matching pieces would be a move into check"); } if (moveCandidates.size() == 1) { return moveCandidates.get(0); } throw new SanParsingException("Ambiguous move, possible are " + moveCandidates); } private void filterIllegalMoves(List<Move> candidates) { for (Iterator<Move> iterator = candidates.iterator(); iterator.hasNext();) { Move move = iterator.next(); Position trialBoard = PositionUtils.makeMove(position, move); if (MoveChecker.canCaptureKing(trialBoard)) { iterator.remove(); } } } }