package me.desht.chesscraft.chess.ai;
import chesspresso.Chess;
import chesspresso.move.IllegalMoveException;
import chesspresso.move.Move;
import me.desht.chesscraft.ChessCraft;
import me.desht.chesscraft.chess.ChessGame;
import me.desht.chesscraft.chess.TimeControl;
import me.desht.chesscraft.chess.TimeControl.RolloverPhase;
import me.desht.chesscraft.exceptions.ChessException;
import me.desht.dhutils.Debugger;
import me.desht.dhutils.LogUtils;
import me.desht.dhutils.MiscUtil;
import org.bukkit.Bukkit;
import org.bukkit.configuration.ConfigurationSection;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class XBoardAI extends ChessAI {
private static final Pattern patternMove =
Pattern.compile("(my)?\\s*move\\s*(is)?\\s*[:>=\\-]?\\s*([a-h][1-8][a-h][1-8][nbrq]?)", Pattern.CASE_INSENSITIVE);
private static final Pattern patternSanMove =
Pattern.compile("(my)?\\s*move\\s*(is)?\\s*[:>=\\-]?\\s*(.+)", Pattern.CASE_INSENSITIVE);
private static final Pattern patternIllegal =
Pattern.compile("(Illegal move.+)|(Error.+)", Pattern.CASE_INSENSITIVE);
private final ExternalIO io;
private final Map<String,String> features = new ConcurrentHashMap<String, String>();
private boolean moveFormatSAN = false;
public XBoardAI(String name, ChessGame chessCraftGame, Boolean isWhite, ConfigurationSection params) {
super(name, chessCraftGame, isWhite, params);
String command = params.getString("command", "gnuchess xboard");
io = new ExternalIO(command);
io.start();
io.writeLine("xboard");
io.writeLine("protover 2");
// this bit gets done asynchronously
new FeatureReader();
}
@Override
public void shutdown() {
io.writeLine("exit");
}
@Override
public void run() {
boolean done = false;
while (!done) {
try {
done = parseCommand();
} catch (IOException e) {
e.printStackTrace();
break;
}
}
}
@Override
public void replayMoves(List<Short> moves) {
// Do nothing here, because we set the position when the AI was created
// using the "setboard" command.
}
@Override
public void offerDraw() {
io.writeLine("draw");
setDrawOfferedToAI(true);
}
public String getFeature(String k) {
return features.containsKey(k) ? features.get(k) : "";
}
private boolean parseCommand() throws IOException {
final String line = io.readLine();
if (line == null) {
aiHasFailed(new IllegalMoveException("illegal move: " + line));
return true;
}
Matcher matcher;
matcher = moveFormatSAN ? patternSanMove.matcher(line) : patternMove.matcher(line);
if (matcher.matches()) {
int fromSqi, toSqi;
if (moveFormatSAN) {
Move m = getChessCraftGame().getMoveFromSAN(matcher.group(3));
if (m == null) {
aiHasFailed(new IllegalMoveException("illegal move: " + line));
return true;
}
fromSqi = m.getFromSqi();
toSqi = m.getToSqi();
} else {
fromSqi = Chess.strToSqi(matcher.group(3).substring(0, 2));
toSqi = Chess.strToSqi(matcher.group(3).substring(2, 4));
}
aiHasMoved(fromSqi, toSqi);
return true;
} else if (line.equals("offer draw")) {
if (isDrawOfferedToAI()) {
// AI has accepted the opponent's draw offer; game over
acceptDrawOffer();
} else {
// AI is making a draw offer - relay this to the other player
makeDrawOffer();
}
return true;
} else {
matcher = patternIllegal.matcher(line);
if (matcher.matches()) {
aiHasFailed(new IllegalMoveException("illegal move: " + line));
return true;
}
}
return false;
}
@Override
public void undoLastMove() {
if (toMove()) {
// stop the AI thinking, and back up one move
io.writeLine("force");
setActive(false);
io.writeLine("undo");
} else {
// undo the AI's last move and the other player's last move
io.writeLine("undo");
io.writeLine("undo");
}
// it is now the other player's move again
}
@Override
protected void movePiece(int fromSqi, int toSqi, boolean otherPlayer) {
// we only send the move if it's the other player doing the move
if (otherPlayer) {
String move = Chess.sqiToStr(fromSqi) + Chess.sqiToStr(toSqi);
io.writeLine(move);
}
}
@Override
public void notifyTimeControl(TimeControl timeControl) {
long totalSecs = timeControl.getTotalTime() / 1000;
long secs = totalSecs % 60;
long mins = totalSecs / 60;
switch (timeControl.getControlType()) {
case MOVE_IN:
io.writeLine("st " + totalSecs);
break;
case GAME_IN:
io.writeLine("level 0 " + mins + ":" + secs + " 0");
break;
case ROLLOVER:
RolloverPhase phase = timeControl.getCurrentPhase();
io.writeLine("level " + phase.getMoves() + " " + phase.getMinutes() + " " + phase.getIncrement() / 1000);
break;
default:
break;
}
}
private class FeatureReader implements Runnable {
private FeatureReader() {
Bukkit.getScheduler().runTaskAsynchronously(ChessCraft.getInstance(), this);
}
private void readLines() {
boolean done = false;
while (!done) {
try {
String s = io.readLine();
Debugger.getInstance().debug(2, "featurereader: [" + s + "]");
if (s.startsWith("feature ")) {
List<String> f = MiscUtil.splitQuotedString(s.replace("=", " "));
for (int i = 1; i < f.size(); i += 2) {
if ((i + 1) >= f.size()) break;
String k = f.get(i);
String v = f.get(i+1);
features.put(k, v);
if (k.equals("done") && v.equals("1")) {
Debugger.getInstance().debug("feature reader done: " + features.size() + " features reported");
done = true;
}
}
}
} catch (IOException e) {
LogUtils.severe("FeatureReader: caught io exception: " + e.getMessage());
done = true;
}
}
setReady();
}
@Override
public void run() {
ExecutorService executor = Executors.newFixedThreadPool(1);
Callable<Void> readTask = new Callable<Void>() {
@Override
public Void call() throws Exception {
readLines();
return null;
}
};
Future<Void> future = executor.submit(readTask);
try {
// we give the AI engine 2 seconds to reply to the "protover" command
// with a list of features
future.get(2000, TimeUnit.MILLISECONDS);
// now it's safe to finish AI init
if (getFeature("san").equals("1")) {
moveFormatSAN = true;
}
if (getFeature("setboard").equals("1")) {
io.writeLine("setboard " + getChessCraftGame().getPosition().getFEN());
if (toMove()) {
io.writeLine("go");
}
} else {
aiHasFailed(new ChessException("This xboard engine doesn't support the 'setboard' feature"));
}
} catch (Exception e) {
aiHasFailed(e);
}
}
}
}