Files
droidfish/buildSrc/src/main/java/chess/GameTree.java

544 lines
20 KiB
Java
Raw Normal View History

2011-11-12 19:44:06 +00:00
/*
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/>.
*/
2019-03-17 22:48:06 +01:00
package chess;
2011-11-12 19:44:06 +00:00
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
2011-11-12 19:44:06 +00:00
public class GameTree {
// Data from the seven tag roster (STR) part of the PGN standard
2019-08-11 11:40:31 +02:00
String event, site, date, round, white, black, result;
2011-11-12 19:44:06 +00:00
public Position startPos;
2011-11-12 19:44:06 +00:00
// Non-standard tags
static private final class TagPair {
2011-11-12 19:44:06 +00:00
String tagName;
String tagValue;
}
private List<TagPair> tagPairs;
2011-11-12 19:44:06 +00:00
public Node rootNode;
public Node currentNode;
public Position currentPos; // Cached value. Computable from "currentNode".
2011-11-12 19:44:06 +00:00
2019-08-11 11:40:31 +02:00
/** Creates an empty GameTree starting at the standard start position. */
public GameTree() {
2011-11-12 19:44:06 +00:00
try {
setStartPos(TextIO.readFEN(TextIO.startPosFEN));
} catch (ChessParseError e) {
}
}
/** Set start position. Drops the whole game tree. */
final void setStartPos(Position pos) {
event = "?";
site = "?";
2019-08-11 11:40:31 +02:00
date = "????.??.??";
2011-11-12 19:44:06 +00:00
round = "?";
white = "?";
black = "?";
startPos = pos;
2019-03-17 11:23:43 +01:00
tagPairs = new ArrayList<>();
2011-11-12 19:44:06 +00:00
rootNode = new Node();
currentNode = rootNode;
currentPos = new Position(startPos);
}
2019-08-11 11:40:31 +02:00
final static private class PgnScanner {
2011-11-12 19:44:06 +00:00
String data;
int idx;
List<PgnToken> savedTokens;
PgnScanner(String pgn) {
2019-03-17 11:23:43 +01:00
savedTokens = new ArrayList<>();
2011-11-12 19:44:06 +00:00
// 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') {
2011-11-12 19:44:06 +00:00
// 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;
2013-12-22 23:20:13 +00:00
StringBuilder sb = new StringBuilder();
2011-11-12 19:44:06 +00:00
while ((c = data.charAt(idx++)) != '}') {
sb.append(c);
}
ret.token = sb.toString();
break;
} else if (c == ';') {
ret.type = PgnToken.COMMENT;
2013-12-22 23:20:13 +00:00
StringBuilder sb = new StringBuilder();
2011-11-12 19:44:06 +00:00
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;
2013-12-22 23:20:13 +00:00
StringBuilder sb = new StringBuilder();
2011-11-12 19:44:06 +00:00
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;
2013-12-22 23:20:13 +00:00
StringBuilder sb = new StringBuilder();
2011-11-12 19:44:06 +00:00
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. */
2019-08-11 11:40:31 +02:00
public final boolean readPGN(String pgn) throws ChessParseError {
2011-11-12 19:44:06 +00:00
PgnScanner scanner = new PgnScanner(pgn);
PgnToken tok = scanner.nextToken();
// Parse tag section
2019-03-17 11:23:43 +01:00
List<TagPair> tagPairs = new ArrayList<>();
2011-11-12 19:44:06 +00:00
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();
2019-08-11 11:40:31 +02:00
Node.parsePgn(scanner, gameRoot);
2011-11-12 19:44:06 +00:00
if (tagPairs.size() == 0) {
gameRoot.verifyChildren(TextIO.readFEN(TextIO.startPosFEN));
if (gameRoot.children.size() == 0)
return false;
}
2011-11-12 19:44:06 +00:00
// Store parsed data in GameTree
2011-11-12 19:44:06 +00:00
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));
2019-08-11 11:40:31 +02:00
result = "";
2011-11-12 19:44:06 +00:00
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")) {
2011-11-12 19:44:06 +00:00
// 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) {
2019-08-11 11:40:31 +02:00
currentNode.verifyChildren(currentPos);
2011-11-12 19:44:06 +00:00
if (variation < 0)
variation = currentNode.defaultChild;
int numChildren = currentNode.children.size();
if (variation >= numChildren)
variation = 0;
2019-08-11 11:40:31 +02:00
currentNode.defaultChild = variation;
2011-11-12 19:44:06 +00:00
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() {
2019-08-11 11:40:31 +02:00
currentNode.verifyChildren(currentPos);
2019-03-17 11:23:43 +01:00
ArrayList<Move> ret = new ArrayList<>();
2011-11-12 19:44:06 +00:00
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.
*/
2019-08-11 11:40:31 +02:00
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.
2011-11-12 19:44:06 +00:00
// 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;
2011-11-12 19:44:06 +00:00
public Node() {
this.moveStr = "";
this.move = null;
this.ui = null;
this.parent = null;
2019-03-17 11:23:43 +01:00
this.children = new ArrayList<>();
2011-11-12 19:44:06 +00:00
this.defaultChild = 0;
this.nag = 0;
this.preComment = "";
this.postComment = "";
}
public Node getParent() {
return parent;
}
2011-11-12 19:44:06 +00:00
/** nodePos must represent the same position as this Node object. */
2019-03-17 11:23:43 +01:00
private boolean verifyChildren(Position nodePos) {
return verifyChildren(nodePos, null);
}
2019-03-17 11:23:43 +01:00
private boolean verifyChildren(Position nodePos, ArrayList<Move> moves) {
2011-11-12 19:44:06 +00:00
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);
2011-11-12 19:44:06 +00:00
if (move != null) {
2019-03-17 22:48:06 +01:00
child.moveStr = TextIO.moveToString(nodePos, move, false, moves);
2011-11-12 19:44:06 +00:00
child.move = move;
child.ui = new UndoInfo();
} else {
anyToRemove = true;
}
}
}
if (anyToRemove) {
2019-03-17 11:23:43 +01:00
ArrayList<Node> validChildren = new ArrayList<>();
2011-11-12 19:44:06 +00:00
for (Node child : children)
if (child.move != null)
validChildren.add(child);
children = validChildren;
}
return anyToRemove;
}
final ArrayList<Integer> getPathFromRoot() {
2019-03-17 11:23:43 +01:00
ArrayList<Integer> ret = new ArrayList<>(64);
2011-11-12 19:44:06 +00:00
Node node = this;
while (node.parent != null) {
ret.add(node.getChildNo());
2011-11-12 19:44:06 +00:00
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();
}
2019-03-17 11:23:43 +01:00
private Node addChild(Node child) {
2011-11-12 19:44:06 +00:00
child.parent = this;
children.add(child);
return child;
}
2019-08-11 11:40:31 +02:00
public static void parsePgn(PgnScanner scanner, Node node) {
2011-11-12 19:44:06 +00:00
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;
}
2019-08-11 11:40:31 +02:00
if (node.parent != null) {
parsePgn(scanner, node.parent);
2011-11-12 19:44:06 +00:00
} 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:
2019-08-11 11:40:31 +02:00
if (moveAdded) { // NAG must be after move
2011-11-12 19:44:06 +00:00
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:
2019-08-11 11:40:31 +02:00
if (moveAdded)
nodeToAdd.postComment += tok.token;
else
nodeToAdd.preComment += tok.token;
2011-11-12 19:44:06 +00:00
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;
}
}
}
}
2011-12-27 14:57:36 +00:00
/** 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);
2019-08-11 11:40:31 +02:00
headers.put("Result", result);
2011-11-12 19:44:06 +00:00
for (int i = 0; i < tagPairs.size(); i++) {
TagPair tp = tagPairs.get(i);
headers.put(tp.tagName, tp.tagValue);
2011-11-12 19:44:06 +00:00
}
}
}