mirror of
https://github.com/peterosterlund2/droidfish.git
synced 2025-12-08 07:11:32 +01:00
544 lines
20 KiB
Java
544 lines
20 KiB
Java
/*
|
|
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 <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
package chess;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
public class GameTree {
|
|
// Data from the seven tag roster (STR) part of the PGN standard
|
|
String event, site, date, round, white, black, result;
|
|
|
|
public Position startPos;
|
|
|
|
// Non-standard tags
|
|
static private final class TagPair {
|
|
String tagName;
|
|
String tagValue;
|
|
}
|
|
private List<TagPair> tagPairs;
|
|
|
|
public Node rootNode;
|
|
public Node currentNode;
|
|
public Position currentPos; // Cached value. Computable from "currentNode".
|
|
|
|
/** Creates an empty GameTree starting at the standard start position. */
|
|
public GameTree() {
|
|
try {
|
|
setStartPos(TextIO.readFEN(TextIO.startPosFEN));
|
|
} catch (ChessParseError e) {
|
|
}
|
|
}
|
|
|
|
/** Set start position. Drops the whole game tree. */
|
|
final void setStartPos(Position pos) {
|
|
event = "?";
|
|
site = "?";
|
|
date = "????.??.??";
|
|
round = "?";
|
|
white = "?";
|
|
black = "?";
|
|
startPos = pos;
|
|
tagPairs = new ArrayList<>();
|
|
rootNode = new Node();
|
|
currentNode = rootNode;
|
|
currentPos = new Position(startPos);
|
|
}
|
|
|
|
final static private class PgnScanner {
|
|
String data;
|
|
int idx;
|
|
List<PgnToken> savedTokens;
|
|
|
|
PgnScanner(String pgn) {
|
|
savedTokens = new ArrayList<>();
|
|
// Skip "escape" lines, ie lines starting with a '%' character
|
|
StringBuilder sb = new StringBuilder();
|
|
int len = pgn.length();
|
|
boolean col0 = true;
|
|
for (int i = 0; i < len; i++) {
|
|
char c = pgn.charAt(i);
|
|
if (c == '%' && col0) {
|
|
while (i + 1 < len) {
|
|
char nextChar = pgn.charAt(i + 1);
|
|
if ((nextChar == '\n') || (nextChar == '\r'))
|
|
break;
|
|
i++;
|
|
}
|
|
col0 = true;
|
|
} else {
|
|
sb.append(c);
|
|
col0 = ((c == '\n') || (c == '\r'));
|
|
}
|
|
}
|
|
sb.append('\n'); // Terminating whitespace simplifies the tokenizer
|
|
data = sb.toString();
|
|
idx = 0;
|
|
}
|
|
|
|
final void putBack(PgnToken tok) {
|
|
savedTokens.add(tok);
|
|
}
|
|
|
|
final PgnToken nextToken() {
|
|
if (savedTokens.size() > 0) {
|
|
int len = savedTokens.size();
|
|
PgnToken ret = savedTokens.get(len - 1);
|
|
savedTokens.remove(len - 1);
|
|
return ret;
|
|
}
|
|
|
|
PgnToken ret = new PgnToken(PgnToken.EOF, null);
|
|
try {
|
|
while (true) {
|
|
char c = data.charAt(idx++);
|
|
if (Character.isWhitespace(c) || c == '\u00a0') {
|
|
// Skip
|
|
} else if (c == '.') {
|
|
ret.type = PgnToken.PERIOD;
|
|
break;
|
|
} else if (c == '*') {
|
|
ret.type = PgnToken.ASTERISK;
|
|
break;
|
|
} else if (c == '[') {
|
|
ret.type = PgnToken.LEFT_BRACKET;
|
|
break;
|
|
} else if (c == ']') {
|
|
ret.type = PgnToken.RIGHT_BRACKET;
|
|
break;
|
|
} else if (c == '(') {
|
|
ret.type = PgnToken.LEFT_PAREN;
|
|
break;
|
|
} else if (c == ')') {
|
|
ret.type = PgnToken.RIGHT_PAREN;
|
|
break;
|
|
} else if (c == '{') {
|
|
ret.type = PgnToken.COMMENT;
|
|
StringBuilder sb = new StringBuilder();
|
|
while ((c = data.charAt(idx++)) != '}') {
|
|
sb.append(c);
|
|
}
|
|
ret.token = sb.toString();
|
|
break;
|
|
} else if (c == ';') {
|
|
ret.type = PgnToken.COMMENT;
|
|
StringBuilder sb = new StringBuilder();
|
|
while (true) {
|
|
c = data.charAt(idx++);
|
|
if ((c == '\n') || (c == '\r'))
|
|
break;
|
|
sb.append(c);
|
|
}
|
|
ret.token = sb.toString();
|
|
break;
|
|
} else if (c == '"') {
|
|
ret.type = PgnToken.STRING;
|
|
StringBuilder sb = new StringBuilder();
|
|
while (true) {
|
|
c = data.charAt(idx++);
|
|
if (c == '"') {
|
|
break;
|
|
} else if (c == '\\') {
|
|
c = data.charAt(idx++);
|
|
}
|
|
sb.append(c);
|
|
}
|
|
ret.token = sb.toString();
|
|
break;
|
|
} else if (c == '$') {
|
|
ret.type = PgnToken.NAG;
|
|
StringBuilder sb = new StringBuilder();
|
|
while (true) {
|
|
c = data.charAt(idx++);
|
|
if (!Character.isDigit(c)) {
|
|
idx--;
|
|
break;
|
|
}
|
|
sb.append(c);
|
|
}
|
|
ret.token = sb.toString();
|
|
break;
|
|
} else { // Start of symbol or integer
|
|
ret.type = PgnToken.SYMBOL;
|
|
StringBuilder sb = new StringBuilder();
|
|
sb.append(c);
|
|
boolean onlyDigits = Character.isDigit(c);
|
|
final String term = ".*[](){;\"$";
|
|
while (true) {
|
|
c = data.charAt(idx++);
|
|
if (Character.isWhitespace(c) || (term.indexOf(c) >= 0)) {
|
|
idx--;
|
|
break;
|
|
}
|
|
sb.append(c);
|
|
if (!Character.isDigit(c))
|
|
onlyDigits = false;
|
|
}
|
|
if (onlyDigits) {
|
|
ret.type = PgnToken.INTEGER;
|
|
}
|
|
ret.token = sb.toString();
|
|
break;
|
|
}
|
|
}
|
|
} catch (StringIndexOutOfBoundsException e) {
|
|
ret.type = PgnToken.EOF;
|
|
}
|
|
return ret;
|
|
}
|
|
|
|
final PgnToken nextTokenDropComments() {
|
|
while (true) {
|
|
PgnToken tok = nextToken();
|
|
if (tok.type != PgnToken.COMMENT)
|
|
return tok;
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Import PGN data. */
|
|
public final boolean readPGN(String pgn) throws ChessParseError {
|
|
PgnScanner scanner = new PgnScanner(pgn);
|
|
PgnToken tok = scanner.nextToken();
|
|
|
|
// Parse tag section
|
|
List<TagPair> tagPairs = new ArrayList<>();
|
|
while (tok.type == PgnToken.LEFT_BRACKET) {
|
|
TagPair tp = new TagPair();
|
|
tok = scanner.nextTokenDropComments();
|
|
if (tok.type != PgnToken.SYMBOL)
|
|
break;
|
|
tp.tagName = tok.token;
|
|
tok = scanner.nextTokenDropComments();
|
|
if (tok.type != PgnToken.STRING)
|
|
break;
|
|
tp.tagValue = tok.token;
|
|
tok = scanner.nextTokenDropComments();
|
|
if (tok.type != PgnToken.RIGHT_BRACKET) {
|
|
// In a well-formed PGN, there is nothing between the string
|
|
// and the right bracket, but broken headers with non-escaped
|
|
// " characters sometimes occur. Try to do something useful
|
|
// for such headers here.
|
|
PgnToken prevTok = new PgnToken(PgnToken.STRING, "");
|
|
while ((tok.type == PgnToken.STRING) || (tok.type == PgnToken.SYMBOL)) {
|
|
if (tok.type != prevTok.type)
|
|
tp.tagValue += '"';
|
|
if ((tok.type == PgnToken.SYMBOL) && (prevTok.type == PgnToken.SYMBOL))
|
|
tp.tagValue += ' ';
|
|
tp.tagValue += tok.token;
|
|
prevTok = tok;
|
|
tok = scanner.nextTokenDropComments();
|
|
}
|
|
}
|
|
tagPairs.add(tp);
|
|
tok = scanner.nextToken();
|
|
}
|
|
scanner.putBack(tok);
|
|
|
|
// Parse move section
|
|
Node gameRoot = new Node();
|
|
Node.parsePgn(scanner, gameRoot);
|
|
|
|
if (tagPairs.size() == 0) {
|
|
gameRoot.verifyChildren(TextIO.readFEN(TextIO.startPosFEN));
|
|
if (gameRoot.children.size() == 0)
|
|
return false;
|
|
}
|
|
|
|
// Store parsed data in GameTree
|
|
String fen = TextIO.startPosFEN;
|
|
int nTags = tagPairs.size();
|
|
for (int i = 0; i < nTags; i++) {
|
|
if (tagPairs.get(i).tagName.equals("FEN")) {
|
|
fen = tagPairs.get(i).tagValue;
|
|
}
|
|
}
|
|
setStartPos(TextIO.readFEN(fen));
|
|
|
|
result = "";
|
|
for (int i = 0; i < nTags; i++) {
|
|
String name = tagPairs.get(i).tagName;
|
|
String val = tagPairs.get(i).tagValue;
|
|
if (name.equals("FEN") || name.equals("SetUp")) {
|
|
// Already handled
|
|
} else if (name.equals("Event")) {
|
|
event = val;
|
|
} else if (name.equals("Site")) {
|
|
site = val;
|
|
} else if (name.equals("Date")) {
|
|
date = val;
|
|
} else if (name.equals("Round")) {
|
|
round = val;
|
|
} else if (name.equals("White")) {
|
|
white = val;
|
|
} else if (name.equals("Black")) {
|
|
black = val;
|
|
} else if (name.equals("Result")) {
|
|
result = val;
|
|
} else {
|
|
this.tagPairs.add(tagPairs.get(i));
|
|
}
|
|
}
|
|
|
|
rootNode = gameRoot;
|
|
currentNode = rootNode;
|
|
|
|
return true;
|
|
}
|
|
|
|
/** Go backward in game tree. */
|
|
public final void goBack() {
|
|
if (currentNode.parent != null) {
|
|
currentPos.unMakeMove(currentNode.move, currentNode.ui);
|
|
currentNode = currentNode.parent;
|
|
}
|
|
}
|
|
|
|
/** Go forward in game tree.
|
|
* @param variation Which variation to follow. -1 to follow default variation.
|
|
*/
|
|
public final void goForward(int variation) {
|
|
currentNode.verifyChildren(currentPos);
|
|
if (variation < 0)
|
|
variation = currentNode.defaultChild;
|
|
int numChildren = currentNode.children.size();
|
|
if (variation >= numChildren)
|
|
variation = 0;
|
|
currentNode.defaultChild = variation;
|
|
if (numChildren > 0) {
|
|
currentNode = currentNode.children.get(variation);
|
|
currentPos.makeMove(currentNode.move, currentNode.ui);
|
|
TextIO.fixupEPSquare(currentPos);
|
|
}
|
|
}
|
|
|
|
/** List of possible continuation moves. */
|
|
public final ArrayList<Move> variations() {
|
|
currentNode.verifyChildren(currentPos);
|
|
ArrayList<Move> ret = new ArrayList<>();
|
|
for (Node child : currentNode.children)
|
|
ret.add(child.move);
|
|
return ret;
|
|
}
|
|
|
|
/**
|
|
* A node object represents a position in the game tree.
|
|
* The position is defined by the move that leads to the position from the parent position.
|
|
* The root node is special in that it doesn't have a move.
|
|
*/
|
|
private static class Node {
|
|
String moveStr; // String representation of move leading to this node. Empty string in root node.
|
|
public Move move; // Computed on demand for better PGN parsing performance.
|
|
// Subtrees of invalid moves will be dropped when detected.
|
|
// Always valid for current node.
|
|
private UndoInfo ui; // Computed when move is computed
|
|
|
|
int nag; // Numeric annotation glyph
|
|
String preComment; // Comment before move
|
|
String postComment; // Comment after move
|
|
|
|
private Node parent; // Null if root node
|
|
int defaultChild;
|
|
private ArrayList<Node> children;
|
|
|
|
public Node() {
|
|
this.moveStr = "";
|
|
this.move = null;
|
|
this.ui = null;
|
|
this.parent = null;
|
|
this.children = new ArrayList<>();
|
|
this.defaultChild = 0;
|
|
this.nag = 0;
|
|
this.preComment = "";
|
|
this.postComment = "";
|
|
}
|
|
|
|
public Node getParent() {
|
|
return parent;
|
|
}
|
|
|
|
/** nodePos must represent the same position as this Node object. */
|
|
private boolean verifyChildren(Position nodePos) {
|
|
return verifyChildren(nodePos, null);
|
|
}
|
|
private boolean verifyChildren(Position nodePos, ArrayList<Move> moves) {
|
|
boolean anyToRemove = false;
|
|
for (Node child : children) {
|
|
if (child.move == null) {
|
|
if (moves == null)
|
|
moves = MoveGen.instance.legalMoves(nodePos);
|
|
Move move = TextIO.stringToMove(nodePos, child.moveStr, moves);
|
|
if (move != null) {
|
|
child.moveStr = TextIO.moveToString(nodePos, move, false, moves);
|
|
child.move = move;
|
|
child.ui = new UndoInfo();
|
|
} else {
|
|
anyToRemove = true;
|
|
}
|
|
}
|
|
}
|
|
if (anyToRemove) {
|
|
ArrayList<Node> validChildren = new ArrayList<>();
|
|
for (Node child : children)
|
|
if (child.move != null)
|
|
validChildren.add(child);
|
|
children = validChildren;
|
|
}
|
|
return anyToRemove;
|
|
}
|
|
|
|
final ArrayList<Integer> getPathFromRoot() {
|
|
ArrayList<Integer> ret = new ArrayList<>(64);
|
|
Node node = this;
|
|
while (node.parent != null) {
|
|
ret.add(node.getChildNo());
|
|
node = node.parent;
|
|
}
|
|
Collections.reverse(ret);
|
|
return ret;
|
|
}
|
|
|
|
/** Return this node's position in the parent node child list. */
|
|
public final int getChildNo() {
|
|
Node p = parent;
|
|
for (int i = 0; i < p.children.size(); i++)
|
|
if (p.children.get(i) == this)
|
|
return i;
|
|
throw new RuntimeException();
|
|
}
|
|
|
|
private Node addChild(Node child) {
|
|
child.parent = this;
|
|
children.add(child);
|
|
return child;
|
|
}
|
|
|
|
public static void parsePgn(PgnScanner scanner, Node node) {
|
|
Node nodeToAdd = new Node();
|
|
boolean moveAdded = false;
|
|
while (true) {
|
|
PgnToken tok = scanner.nextToken();
|
|
switch (tok.type) {
|
|
case PgnToken.INTEGER:
|
|
case PgnToken.PERIOD:
|
|
break;
|
|
case PgnToken.LEFT_PAREN:
|
|
if (moveAdded) {
|
|
node = node.addChild(nodeToAdd);
|
|
nodeToAdd = new Node();
|
|
moveAdded = false;
|
|
}
|
|
if (node.parent != null) {
|
|
parsePgn(scanner, node.parent);
|
|
} else {
|
|
int nestLevel = 1;
|
|
while (nestLevel > 0) {
|
|
switch (scanner.nextToken().type) {
|
|
case PgnToken.LEFT_PAREN: nestLevel++; break;
|
|
case PgnToken.RIGHT_PAREN: nestLevel--; break;
|
|
case PgnToken.EOF: return; // Broken PGN file. Just give up.
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case PgnToken.NAG:
|
|
if (moveAdded) { // NAG must be after move
|
|
try {
|
|
nodeToAdd.nag = Integer.parseInt(tok.token);
|
|
} catch (NumberFormatException e) {
|
|
nodeToAdd.nag = 0;
|
|
}
|
|
}
|
|
break;
|
|
case PgnToken.SYMBOL:
|
|
if (tok.token.equals("1-0") || tok.token.equals("0-1") || tok.token.equals("1/2-1/2")) {
|
|
if (moveAdded) node.addChild(nodeToAdd);
|
|
return;
|
|
}
|
|
char lastChar = tok.token.charAt(tok.token.length() - 1);
|
|
if (lastChar == '+')
|
|
tok.token = tok.token.substring(0, tok.token.length() - 1);
|
|
if ((lastChar == '!') || (lastChar == '?')) {
|
|
int movLen = tok.token.length() - 1;
|
|
while (movLen > 0) {
|
|
char c = tok.token.charAt(movLen - 1);
|
|
if ((c == '!') || (c == '?'))
|
|
movLen--;
|
|
else
|
|
break;
|
|
}
|
|
String ann = tok.token.substring(movLen);
|
|
tok.token = tok.token.substring(0, movLen);
|
|
int nag = 0;
|
|
if (ann.equals("!")) nag = 1;
|
|
else if (ann.equals("?")) nag = 2;
|
|
else if (ann.equals("!!")) nag = 3;
|
|
else if (ann.equals("??")) nag = 4;
|
|
else if (ann.equals("!?")) nag = 5;
|
|
else if (ann.equals("?!")) nag = 6;
|
|
if (nag > 0)
|
|
scanner.putBack(new PgnToken(PgnToken.NAG, Integer.valueOf(nag).toString()));
|
|
}
|
|
if (tok.token.length() > 0) {
|
|
if (moveAdded) {
|
|
node = node.addChild(nodeToAdd);
|
|
nodeToAdd = new Node();
|
|
moveAdded = false;
|
|
}
|
|
nodeToAdd.moveStr = tok.token;
|
|
moveAdded = true;
|
|
}
|
|
break;
|
|
case PgnToken.COMMENT:
|
|
if (moveAdded)
|
|
nodeToAdd.postComment += tok.token;
|
|
else
|
|
nodeToAdd.preComment += tok.token;
|
|
break;
|
|
case PgnToken.ASTERISK:
|
|
case PgnToken.LEFT_BRACKET:
|
|
case PgnToken.RIGHT_BRACKET:
|
|
case PgnToken.STRING:
|
|
case PgnToken.RIGHT_PAREN:
|
|
case PgnToken.EOF:
|
|
if (moveAdded) node.addChild(nodeToAdd);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/** Get PGN header tags and values. */
|
|
public void getHeaders(Map<String,String> headers) {
|
|
headers.put("Event", event);
|
|
headers.put("Site", site);
|
|
headers.put("Date", date);
|
|
headers.put("Round", round);
|
|
headers.put("White", white);
|
|
headers.put("Black", black);
|
|
headers.put("Result", result);
|
|
for (int i = 0; i < tagPairs.size(); i++) {
|
|
TagPair tp = tagPairs.get(i);
|
|
headers.put(tp.tagName, tp.tagValue);
|
|
}
|
|
}
|
|
}
|