/*
DroidFish - An Android chess program.
Copyright (C) 2011 Peter Ă–sterlund, peterosterlund2@gmail.com
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 3 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, see .
*/
package chess;
import java.util.ArrayList;
import java.util.List;
/** Handle conversion of positions and moves to/from text format. */
public class TextIO {
static public final String startPosFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
/** Parse a FEN string and return a chess Position object. */
public static Position readFEN(String fen) throws ChessParseError {
fen = fen.trim();
Position pos = new Position();
String[] words = fen.split(" ");
if (words.length < 2)
throw new ChessParseError("too few pieces");
for (int i = 0; i < words.length; i++) {
words[i] = words[i].trim();
}
// Piece placement
int row = 7;
int col = 0;
for (int i = 0; i < words[0].length(); i++) {
char c = words[0].charAt(i);
switch (c) {
case '1': col += 1; break;
case '2': col += 2; break;
case '3': col += 3; break;
case '4': col += 4; break;
case '5': col += 5; break;
case '6': col += 6; break;
case '7': col += 7; break;
case '8': col += 8; break;
case '/': row--; col = 0; break;
case 'P': safeSetPiece(pos, col, row, Piece.WPAWN); col++; break;
case 'N': safeSetPiece(pos, col, row, Piece.WKNIGHT); col++; break;
case 'B': safeSetPiece(pos, col, row, Piece.WBISHOP); col++; break;
case 'R': safeSetPiece(pos, col, row, Piece.WROOK); col++; break;
case 'Q': safeSetPiece(pos, col, row, Piece.WQUEEN); col++; break;
case 'K': safeSetPiece(pos, col, row, Piece.WKING); col++; break;
case 'p': safeSetPiece(pos, col, row, Piece.BPAWN); col++; break;
case 'n': safeSetPiece(pos, col, row, Piece.BKNIGHT); col++; break;
case 'b': safeSetPiece(pos, col, row, Piece.BBISHOP); col++; break;
case 'r': safeSetPiece(pos, col, row, Piece.BROOK); col++; break;
case 'q': safeSetPiece(pos, col, row, Piece.BQUEEN); col++; break;
case 'k': safeSetPiece(pos, col, row, Piece.BKING); col++; break;
default: throw new ChessParseError("invalid piece", pos);
}
}
if (words[1].length() > 0) {
boolean wtm;
switch (words[1].charAt(0)) {
case 'w': wtm = true; break;
case 'b': wtm = false; break;
default: throw new ChessParseError("invalid side", pos);
}
pos.setWhiteMove(wtm);
} else {
throw new ChessParseError("invalid side", pos);
}
// Castling rights
int castleMask = 0;
if (words.length > 2) {
for (int i = 0; i < words[2].length(); i++) {
char c = words[2].charAt(i);
switch (c) {
case 'K':
castleMask |= (1 << Position.H1_CASTLE);
break;
case 'Q':
castleMask |= (1 << Position.A1_CASTLE);
break;
case 'k':
castleMask |= (1 << Position.H8_CASTLE);
break;
case 'q':
castleMask |= (1 << Position.A8_CASTLE);
break;
case '-':
break;
default:
throw new ChessParseError("invalid castling flags", pos);
}
}
}
pos.setCastleMask(castleMask);
if (words.length > 3) {
// En passant target square
String epString = words[3];
if (!epString.equals("-")) {
if (epString.length() < 2)
throw new ChessParseError("invalid en passant square", pos);
int epSq = getSquare(epString);
if (epSq != -1) {
if (pos.whiteMove) {
if ((Position.getY(epSq) != 5) || (pos.getPiece(epSq) != Piece.EMPTY) ||
(pos.getPiece(epSq - 8) != Piece.BPAWN))
epSq = -1;
} else {
if ((Position.getY(epSq) != 2) || (pos.getPiece(epSq) != Piece.EMPTY) ||
(pos.getPiece(epSq + 8) != Piece.WPAWN))
epSq = -1;
}
pos.setEpSquare(epSq);
}
}
}
try {
if (words.length > 4) {
pos.halfMoveClock = Integer.parseInt(words[4]);
}
if (words.length > 5) {
pos.fullMoveCounter = Integer.parseInt(words[5]);
}
} catch (NumberFormatException nfe) {
// Ignore errors here, since the fields are optional
}
// Each side must have exactly one king
int[] nPieces = new int[Piece.nPieceTypes];
for (int i = 0; i < Piece.nPieceTypes; i++)
nPieces[i] = 0;
for (int x = 0; x < 8; x++)
for (int y = 0; y < 8; y++)
nPieces[pos.getPiece(Position.getSquare(x, y))]++;
if (nPieces[Piece.WKING] != 1)
throw new ChessParseError("white num kings", pos);
if (nPieces[Piece.BKING] != 1)
throw new ChessParseError("black num kings", pos);
// White must not have too many pieces
int maxWPawns = 8;
maxWPawns -= Math.max(0, nPieces[Piece.WKNIGHT] - 2);
maxWPawns -= Math.max(0, nPieces[Piece.WBISHOP] - 2);
maxWPawns -= Math.max(0, nPieces[Piece.WROOK ] - 2);
maxWPawns -= Math.max(0, nPieces[Piece.WQUEEN ] - 1);
if (nPieces[Piece.WPAWN] > maxWPawns)
throw new ChessParseError("too many white pieces", pos);
// Black must not have too many pieces
int maxBPawns = 8;
maxBPawns -= Math.max(0, nPieces[Piece.BKNIGHT] - 2);
maxBPawns -= Math.max(0, nPieces[Piece.BBISHOP] - 2);
maxBPawns -= Math.max(0, nPieces[Piece.BROOK ] - 2);
maxBPawns -= Math.max(0, nPieces[Piece.BQUEEN ] - 1);
if (nPieces[Piece.BPAWN] > maxBPawns)
throw new ChessParseError("too many black pieces", pos);
// Make sure king can not be captured
Position pos2 = new Position(pos);
pos2.setWhiteMove(!pos.whiteMove);
if (MoveGen.inCheck(pos2)) {
throw new ChessParseError("king capture possible", pos);
}
fixupEPSquare(pos);
return pos;
}
/** Remove pseudo-legal EP square if it is not legal, ie would leave king in check. */
public static void fixupEPSquare(Position pos) {
int epSquare = pos.getEpSquare();
if (epSquare >= 0) {
ArrayList moves = MoveGen.instance.legalMoves(pos);
boolean epValid = false;
for (Move m : moves) {
if (m.to == epSquare) {
if (pos.getPiece(m.from) == (pos.whiteMove ? Piece.WPAWN : Piece.BPAWN)) {
epValid = true;
break;
}
}
}
if (!epValid)
pos.setEpSquare(-1);
}
}
private static void safeSetPiece(Position pos, int col, int row, int p) throws ChessParseError {
if (row < 0) throw new ChessParseError("too many rows");
if (col > 7) throw new ChessParseError("too many columns");
if ((p == Piece.WPAWN) || (p == Piece.BPAWN)) {
if ((row == 0) || (row == 7))
throw new ChessParseError("pawn on first last rank");
}
pos.setPiece(Position.getSquare(col, row), p);
}
/**
* Convert a chess move to human readable form.
* @param pos The chess position.
* @param move The executed move.
* @param longForm If true, use long notation, eg Ng1-f3.
* Otherwise, use short notation, eg Nf3.
*/
public static String moveToString(Position pos, Move move, boolean longForm) {
return moveToString(pos, move, longForm, null);
}
public static String moveToString(Position pos, Move move, boolean longForm,
List moves) {
if ((move == null) || move.equals(new Move(0, 0, 0)))
return "--";
StringBuilder ret = new StringBuilder();
int wKingOrigPos = Position.getSquare(4, 0);
int bKingOrigPos = Position.getSquare(4, 7);
if (move.from == wKingOrigPos && pos.getPiece(wKingOrigPos) == Piece.WKING) {
// Check white castle
if (move.to == Position.getSquare(6, 0)) {
ret.append("O-O");
} else if (move.to == Position.getSquare(2, 0)) {
ret.append("O-O-O");
}
} else if (move.from == bKingOrigPos && pos.getPiece(bKingOrigPos) == Piece.BKING) {
// Check black castle
if (move.to == Position.getSquare(6, 7)) {
ret.append("O-O");
} else if (move.to == Position.getSquare(2, 7)) {
ret.append("O-O-O");
}
}
if (ret.length() == 0) {
int p = pos.getPiece(move.from);
ret.append(pieceToChar(p));
int x1 = Position.getX(move.from);
int y1 = Position.getY(move.from);
int x2 = Position.getX(move.to);
int y2 = Position.getY(move.to);
if (longForm) {
ret.append((char)(x1 + 'a'));
ret.append((char) (y1 + '1'));
ret.append(isCapture(pos, move) ? 'x' : '-');
} else {
if (p == (pos.whiteMove ? Piece.WPAWN : Piece.BPAWN)) {
if (isCapture(pos, move)) {
ret.append((char) (x1 + 'a'));
}
} else {
int numSameTarget = 0;
int numSameFile = 0;
int numSameRow = 0;
if (moves == null)
moves = MoveGen.instance.legalMoves(pos);
int mSize = moves.size();
for (int mi = 0; mi < mSize; mi++) {
Move m = moves.get(mi);
if ((pos.getPiece(m.from) == p) && (m.to == move.to)) {
numSameTarget++;
if (Position.getX(m.from) == x1)
numSameFile++;
if (Position.getY(m.from) == y1)
numSameRow++;
}
}
if (numSameTarget < 2) {
// No file/row info needed
} else if (numSameFile < 2) {
ret.append((char) (x1 + 'a')); // Only file info needed
} else if (numSameRow < 2) {
ret.append((char) (y1 + '1')); // Only row info needed
} else {
ret.append((char) (x1 + 'a')); // File and row info needed
ret.append((char) (y1 + '1'));
}
}
if (isCapture(pos, move)) {
ret.append('x');
}
}
ret.append((char) (x2 + 'a'));
ret.append((char) (y2 + '1'));
if (move.promoteTo != Piece.EMPTY)
ret.append(pieceToChar(move.promoteTo));
}
UndoInfo ui = new UndoInfo();
pos.makeMove(move, ui);
boolean givesCheck = MoveGen.inCheck(pos);
if (givesCheck) {
ArrayList nextMoves = MoveGen.instance.legalMoves(pos);
if (nextMoves.size() == 0) {
ret.append('#');
} else {
ret.append('+');
}
}
pos.unMakeMove(move, ui);
return ret.toString();
}
private static boolean isCapture(Position pos, Move move) {
if (pos.getPiece(move.to) == Piece.EMPTY) {
int p = pos.getPiece(move.from);
if ((p == (pos.whiteMove ? Piece.WPAWN : Piece.BPAWN)) && (move.to == pos.getEpSquare())) {
return true;
} else {
return false;
}
} else {
return true;
}
}
private final static class MoveInfo {
int piece; // -1 for unspecified
int fromX, fromY, toX, toY; // -1 for unspecified
int promPiece; // -1 for unspecified
MoveInfo() { piece = fromX = fromY = toX = toY = promPiece = -1; }
}
/**
* Convert a chess move string to a Move object.
* The string may specify any combination of piece/source/target/promotion
* information as long as it matches exactly one valid move.
*/
public static Move stringToMove(Position pos, String strMove) {
return stringToMove(pos, strMove, null);
}
public static Move stringToMove(Position pos, String strMove,
ArrayList moves) {
if (strMove.equals("--"))
return new Move(0, 0, 0);
strMove = strMove.replaceAll("=", "");
strMove = strMove.replaceAll("\\+", "");
strMove = strMove.replaceAll("#", "");
boolean wtm = pos.whiteMove;
MoveInfo info = new MoveInfo();
boolean capture = false;
if (strMove.equals("O-O") || strMove.equals("0-0") || strMove.equals("o-o")) {
info.piece = wtm ? Piece.WKING : Piece.BKING;
info.fromX = 4;
info.toX = 6;
info.fromY = info.toY = wtm ? 0 : 7;
info.promPiece = Piece.EMPTY;
} else if (strMove.equals("O-O-O") || strMove.equals("0-0-0") || strMove.equals("o-o-o")) {
info.piece = wtm ? Piece.WKING : Piece.BKING;
info.fromX = 4;
info.toX = 2;
info.fromY = info.toY = wtm ? 0 : 7;
info.promPiece = Piece.EMPTY;
} else {
boolean atToSq = false;
for (int i = 0; i < strMove.length(); i++) {
char c = strMove.charAt(i);
if (i == 0) {
int piece = charToPiece(wtm, c);
if (piece >= 0) {
info.piece = piece;
continue;
}
}
int tmpX = c - 'a';
if ((tmpX >= 0) && (tmpX < 8)) {
if (atToSq || (info.fromX >= 0))
info.toX = tmpX;
else
info.fromX = tmpX;
}
int tmpY = c - '1';
if ((tmpY >= 0) && (tmpY < 8)) {
if (atToSq || (info.fromY >= 0))
info.toY = tmpY;
else
info.fromY = tmpY;
}
if ((c == 'x') || (c == '-')) {
atToSq = true;
if (c == 'x')
capture = true;
}
if (i == strMove.length() - 1) {
int promPiece = charToPiece(wtm, c);
if (promPiece >= 0) {
info.promPiece = promPiece;
}
}
}
if ((info.fromX >= 0) && (info.toX < 0)) {
info.toX = info.fromX;
info.fromX = -1;
}
if ((info.fromY >= 0) && (info.toY < 0)) {
info.toY = info.fromY;
info.fromY = -1;
}
if (info.piece < 0) {
boolean haveAll = (info.fromX >= 0) && (info.fromY >= 0) &&
(info.toX >= 0) && (info.toY >= 0);
if (!haveAll)
info.piece = wtm ? Piece.WPAWN : Piece.BPAWN;
}
if (info.promPiece < 0)
info.promPiece = Piece.EMPTY;
}
if (moves == null)
moves = MoveGen.instance.legalMoves(pos);
ArrayList matches = new ArrayList<>(2);
for (int i = 0; i < moves.size(); i++) {
Move m = moves.get(i);
int p = pos.getPiece(m.from);
boolean match = true;
if ((info.piece >= 0) && (info.piece != p))
match = false;
if ((info.fromX >= 0) && (info.fromX != Position.getX(m.from)))
match = false;
if ((info.fromY >= 0) && (info.fromY != Position.getY(m.from)))
match = false;
if ((info.toX >= 0) && (info.toX != Position.getX(m.to)))
match = false;
if ((info.toY >= 0) && (info.toY != Position.getY(m.to)))
match = false;
if ((info.promPiece >= 0) && (info.promPiece != m.promoteTo))
match = false;
if (match) {
matches.add(m);
}
}
int nMatches = matches.size();
if (nMatches == 0)
return null;
else if (nMatches == 1)
return matches.get(0);
if (!capture)
return null;
Move move = null;
for (int i = 0; i < matches.size(); i++) {
Move m = matches.get(i);
int capt = pos.getPiece(m.to);
if (capt != Piece.EMPTY) {
if (move == null)
move = m;
else
return null;
}
}
return move;
}
/**
* Convert a string, such as "e4" to a square number.
* @return The square number, or -1 if not a legal square.
*/
public static int getSquare(String s) {
int x = s.charAt(0) - 'a';
int y = s.charAt(1) - '1';
if ((x < 0) || (x > 7) || (y < 0) || (y > 7))
return -1;
return Position.getSquare(x, y);
}
private static String pieceToChar(int p) {
switch (p) {
case Piece.WQUEEN: case Piece.BQUEEN: return "Q";
case Piece.WROOK: case Piece.BROOK: return "R";
case Piece.WBISHOP: case Piece.BBISHOP: return "B";
case Piece.WKNIGHT: case Piece.BKNIGHT: return "N";
case Piece.WKING: case Piece.BKING: return "K";
}
return "";
}
private static int charToPiece(boolean white, char c) {
switch (c) {
case 'Q': case 'q': return white ? Piece.WQUEEN : Piece.BQUEEN;
case 'R': case 'r': return white ? Piece.WROOK : Piece.BROOK;
case 'B': return white ? Piece.WBISHOP : Piece.BBISHOP;
case 'N': case 'n': return white ? Piece.WKNIGHT : Piece.BKNIGHT;
case 'K': case 'k': return white ? Piece.WKING : Piece.BKING;
case 'P': case 'p': return white ? Piece.WPAWN : Piece.BPAWN;
}
return -1;
}
}