/**
* Calculon - A Java chess-engine.
*
* Copyright (C) 2008-2009 Barry Smith
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
* implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package nl.zoidberg.calculon.icc;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Stack;
import java.util.StringTokenizer;
import java.util.logging.ConsoleHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.Logger;
import nl.zoidberg.calculon.engine.BitBoard;
import nl.zoidberg.calculon.engine.ChessEngine;
import nl.zoidberg.calculon.engine.MoveGenerator;
import nl.zoidberg.calculon.model.Piece;
import nl.zoidberg.calculon.notation.PGNUtils;
import nl.zoidberg.calculon.notation.Style12;
import nl.zoidberg.calculon.opening.OpeningBook;
import nl.zoidberg.calculon.util.LogFormatter;
import org.apache.commons.digester.Digester;
import org.apache.commons.lang.StringUtils;
public class ICCInterface {
private static final Logger log = Logger.getLogger(ICCInterface.class.getName());
private static final int DG_MY_GAME_STARTED = 15;
private static final int DG_MY_GAME_RESULT = 16;
private static boolean shutdown = false;
private static String talkResponse = "I'm sorry Dave, I'm afraid I can't do that.";
private static ICCSConfig iccConfig;
private static enum GameType { BULLET, BLITZ, STANDARD };
private Socket connection;
private Thread moveThread = null;
private List<ConnectionListener> listeners = new ArrayList<ConnectionListener>();
private List<BlockHandler> blockHandlers = new ArrayList<BlockHandler>();
private PrintStream out;
private String opponent = null;
private boolean rated = false;
private int gameNumber = -1;
private boolean playingWhite = true;
private boolean accept = true;
private boolean alive = true;
private BitBoard currentBoard;
private OpeningBook openingBook;
private GameType gameType;
public static void main(String[] args) throws Exception {
log.setLevel(Level.FINE);
Handler handler = new ConsoleHandler();
handler.setLevel(Level.FINE);
handler.setFormatter(new LogFormatter());
for(Handler h: log.getHandlers()) {
log.removeHandler(h);
}
log.addHandler(handler);
if(System.getProperty("calculon.password") == null)
{
log.severe("password must be specified.");
System.exit(-1);
}
while(!shutdown) {
try {
new ICCInterface().connect();
} catch (Exception x) {
log.log(Level.SEVERE, "Error", x);
try { Thread.sleep(60000); } catch (InterruptedException ix) { }
}
}
}
private ICCInterface() {
Digester digester = new Digester();
digester.addObjectCreate("calculon/icc", ICCSConfig.class);
digester.addBeanPropertySetter("calculon/icc/operator-name", "operatorName");
digester.addBeanPropertySetter("calculon/icc/login-name", "loginName");
digester.addBeanPropertySetter("calculon/icc/accept-min", "acceptMin");
digester.addBeanPropertySetter("calculon/icc/accept-max", "acceptMax");
digester.addBeanPropertySetter("calculon/icc/max-rematches", "maxRematches");
digester.addBeanPropertySetter("calculon/icc/reseek", "reseek");
digester.addObjectCreate("calculon/icc/default-seeks/seek", ICCSConfig.Seek.class);
digester.addSetProperties("calculon/icc/default-seeks/seek", "time", "initialTime");
digester.addSetProperties("calculon/icc/default-seeks/seek", "inc", "increment");
digester.addSetNext("calculon/icc/default-seeks/seek", "addSeekAd");
try {
iccConfig = (ICCSConfig) digester.parse(ClassLoader.getSystemResourceAsStream("calculon.xml"));
} catch (Exception e) {
log.log(Level.WARNING, "Config reading failed", e);
throw new RuntimeException(e);
}
log.finer(iccConfig.toString());
openingBook = OpeningBook.getDefaultBook();
listeners.add(new DebugListener());
listeners.add(new ChallengeListener());
listeners.add(new BoardListener());
listeners.add(new AbortListener());
listeners.add(new CommandListener());
listeners.add(new ReseekListener());
listeners.add(new ChatListener());
listeners.add(new BlockListener());
listeners.add(new BlockLv2Listener());
blockHandlers.add(new GameEndedHandler());
}
public void connect() throws IOException {
connection = new Socket("chessclub.com", 23);
doLogin();
BufferedReader reader = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
out = new PrintStream(connection.getOutputStream());
send("set level1 1");
send("set style 12");
receiveLevel2(DG_MY_GAME_STARTED);
receiveLevel2(DG_MY_GAME_RESULT);
setStatus();
if (iccConfig.isReseek()) {
reseek();
}
Runnable keepAlive = new Runnable() {
public void run() {
while(alive) {
send("date");
try { Thread.sleep(60000 * 15); } catch (InterruptedException x) { }
}
}
};
Thread keepAliveThread = new Thread(keepAlive);
keepAliveThread.setDaemon(true);
keepAliveThread.start();
StringBuffer line = new StringBuffer();
int c;
try {
while((c = reader.read()) != -1) {
if(c == ('M'&0x1F) || c == ('G'&0x1F)) {
continue;
}
line.append((char)c);
if(c == '\n') {
fireDataReceived(line.toString());
line.setLength(0);
continue;
}
if(line.length() >= 2 && line.charAt(line.length()-2) == ('Y'&0x1F) && line.charAt(line.length()-1) == ']') {
fireDataReceived(line.toString());
line.setLength(0);
}
}
} finally {
alive = false;
try {
reader.close();
out.close();
} catch (Exception x) { }
}
}
private void receiveLevel2(int dgId) {
send("set-2 " + String.valueOf(dgId) + " 1");
}
private void fireDataReceived(String s) {
for (ConnectionListener listener : listeners) {
try {
listener.message(s);
} catch (Exception e) {
log.log(Level.WARNING, "Handler " + listener + " threw exception", e);
}
}
}
private void doLogin() throws IOException {
int c;
String sLogin = "login: ";
int sptr = 0;
while ((c = connection.getInputStream().read()) != -1) {
if (c == sLogin.charAt(sptr)) {
sptr++;
if (sptr == sLogin.length()) {
log.fine("Sending login name");
connection.getOutputStream()
.write((iccConfig.getLoginName() + "\n").getBytes());
break;
}
} else {
sptr = 0;
}
}
sLogin = "password: ";
sptr = 0;
while ((c = connection.getInputStream().read()) != -1) {
if (c == sLogin.charAt(sptr)) {
sptr++;
if (sptr == sLogin.length()) {
log.finer("Sending password (" + System.getProperty("calculon.password") + ")");
connection.getOutputStream().write((System.getProperty("calculon.password") + "\n").getBytes());
break;
}
} else {
sptr = 0;
}
}
}
private void reseek() {
send("resume");
Runnable seeker = new Runnable() {
public void run() {
for(int i = 0; i < 4; i++) {
try { Thread.sleep(15000); } catch (InterruptedException x) { }
if(gameNumber != -1) {
return;
}
send("resume");
}
for(ICCSConfig.Seek seek: iccConfig.getSeekAds()) {
send(seek.getCommand());
}
}
};
new Thread(seeker).start();
}
private synchronized void send(String s) {
log.fine(">>> " + s);
out.println(s);
}
private void tellOp(String s) {
send("tell " + iccConfig.getOperatorName() + " " + s);
}
private void setStatus() {
if(shutdown) {
send("set 9 Current Status: Shutting down.");
} else if (iccConfig.isReseek()) {
send("set 9 Current Status: Auto (accept " + (accept?"on":"off") + ").");
} else {
send("set 9 Current Status: Manual (accept " + (accept?"on":"off") + ").");
}
}
private interface ConnectionListener {
public void message(String s);
}
private interface BlockHandler {
@Deprecated
public void processBlock(ResponseBlock responseBlock);
public void processBlock(ResponseBlockLv2 responseBlock);
}
private class DebugListener implements ConnectionListener {
public void message(String s) {
log.finest("<<< " + s);
}
}
private class ReseekListener implements ConnectionListener {
public void message(String s) {
}
}
private class ChallengeListener implements ConnectionListener {
// e.g. Challenge: BarryNL (2029) CalculonX (2000) rated Blitz 5 0
public void message(String s) {
if (s.startsWith("Challenge: ") && !accept) {
send("decline");
return;
}
if(s.startsWith("Challenge: ") && s.contains(" (adjourned)")) {
log.fine("Accepting adjourned game.");
send("accept");
return;
}
if (s.startsWith("Challenge: ") && accept) {
String[] args = StringUtils.split(s);
int gameLength = Integer.parseInt(args[args.length-2])*60 + Integer.parseInt(args[args.length-1])*40;
if("rated".equals(args[args.length-4])
&& gameLength >= iccConfig.getAcceptMin() && gameLength <= iccConfig.getAcceptMax()) {
log.fine("Accepting: '" + s + "' " + gameLength + "s");
send("accept");
} else {
log.fine("Rejecting: '" + s + "' " + gameLength + "s");
send("decline");
}
return;
}
if (s.startsWith("Creating: ")) {
log.info("Starting game: '" + s + "'");
List<String> fields = Arrays.asList(StringUtils.split(s));
playingWhite = iccConfig.getLoginName().equals(fields.get(1));
opponent = playingWhite ? fields.get(3) : fields.get(1);
rated = "rated".equals(fields.get(5));
send("finger " + opponent);
if("Bullet".equals(fields.get(6))) {
gameType = GameType.BULLET;
} else if("Blitz".equals(fields.get(6))) {
gameType = GameType.BLITZ;
} else if("Standard".equals(fields.get(6))) {
gameType = GameType.STANDARD;
}
}
}
}
private class AbortListener implements ConnectionListener {
public void message(String s) {
if (opponent != null
&& s.startsWith(opponent + " would like to abort the game;")
&& !rated) {
send("abort");
}
}
}
private class ChatListener implements ConnectionListener {
public void message(String s) {
if(s.startsWith(iccConfig.getOperatorName() + " ")) {
return;
}
String[] fields = StringUtils.split(s);
if(fields.length >= 3 && "tells".equals(fields[1]) && "you:".equals(fields[2])) {
send("tell " + fields[0] + " " + talkResponse);
}
if(fields.length >= 3 && "says:".equals(fields[1])) {
send("say " + talkResponse);
}
}
}
private class CommandListener implements ConnectionListener {
public void message(String s) {
if (!s.startsWith(iccConfig.getOperatorName() + " tells you: ")) {
return;
}
List<String> words = Arrays.asList(StringUtils.split(s));
if (words.size() < 4) {
return;
}
if ("do".equals(words.get(3))) {
StringBuffer buf = new StringBuffer();
for (int i = 4; i < words.size(); i++) {
buf.append(words.get(i)).append(" ");
}
send(buf.toString().trim());
tellOp("sent '" + buf.toString().trim() + "'.");
}
if ("shutdown".equals(words.get(3))) {
tellOp("Will shutdown after current game.");
shutdown = true;
iccConfig.setReseek(false);
accept = false;
setStatus();
}
if ("accept".equals(words.get(3))) {
if(words.size() > 4 && "on".equals(words.get(4))) {
accept = true;
shutdown = false;
} else {
accept = false;
}
tellOp("accept " + (accept ? "on" : "off"));
setStatus();
}
if (words.size() > 4 && "reseek".equals(words.get(3))) {
if("on".equals(words.get(4))) {
iccConfig.setReseek(true);
shutdown = false;
} else {
iccConfig.setReseek(false);
}
tellOp("reseek " + (iccConfig.isReseek() ? "on" : "off"));
setStatus();
}
}
}
private class GameEndedHandler implements BlockHandler {
public void processBlock(ResponseBlock responseBlock) {
// Deprecated
}
public void processBlock(ResponseBlockLv2 responseBlock) {
if(responseBlock.getCode() != DG_MY_GAME_RESULT) {
return;
}
log.info("Game ends: [" + responseBlock.getData() + "]");
currentBoard = null;
gameNumber = -1;
opponent = null;
while(moveThread != null && moveThread.isAlive()) {
try { Thread.sleep(200); } catch (InterruptedException x) { }
}
if (shutdown) {
send("quit");
} else if (iccConfig.isReseek()) {
reseek();
}
}
}
private class BoardListener implements ConnectionListener {
public void message(String s) {
if (!s.startsWith("<12> ")) {
return;
}
final Style12 style12 = new Style12(s);
if(style12.isMyGame()) {
gameNumber = style12.getGameNumber();
opponent = style12.getOpponentName();
playingWhite = (style12.getMyColor() == Piece.WHITE);
if(style12.isInitialPosition()) {
currentBoard = new BitBoard().initialise();
}
}
if ( ! (style12.getMyRelationToGame() == Style12.REL_ME_TO_MOVE)) {
return;
}
if(style12.isFlagged()) {
gameNumber = -1;
currentBoard = null;
return;
}
if(style12.getHalfMoveCount() >= 100) {
log.info("Claiming draw by 50-move rule");
send("draw");
return;
}
if(currentBoard != null && !"none".equals(style12.getPreviousMovePGN())) {
try {
currentBoard.makeMove(currentBoard.getMove(PGNUtils.toPgnMoveMap(currentBoard).get(style12.getPreviousMovePGN())));
} catch (Exception x) {
log.log(Level.SEVERE, "Apply move failed", x);
}
}
if(currentBoard == null || ! currentBoard.equalPosition(style12.getBoard())) {
log.warning("Out of sync board detected - resetting!");
currentBoard = style12.getBoard();
}
if(currentBoard.getRepeatedCount() >= 3) {
log.info("Claiming draw by 3-fold repitition (opp move)");
send("draw");
return;
}
String bookMove = openingBook.getBookMove(currentBoard);
if(bookMove != null) {
PGNUtils.applyMove(currentBoard, bookMove);
send(bookMove);
log.fine("Using book move: " + bookMove);
return;
}
if( ! new MoveGenerator(currentBoard).hasNext()) {
log.fine("Looks like that game is over!");
return;
}
Runnable moveMaker = new Runnable() {
public void run() {
BitBoard myBoard = currentBoard;
ChessEngine engine = new ChessEngine();
if(style12.getGameTime() >= 900) {
engine.setQuiesce(true);
}
String bestMove = engine.getPreferredMove(myBoard);
if(bestMove != null) {
if(gameNumber != -1) {
log.info("Moving: " + PGNUtils.translateMove(myBoard, bestMove));
if(currentBoard != null) {
currentBoard.makeMove(currentBoard.getMove(bestMove));
}
send(bestMove.toLowerCase());
if(currentBoard.getRepeatedCount() >= 3) {
log.info("Claiming draw by 3-fold repitition (my move)");
send("draw");
}
} else {
log.info("Game not active - move aborted");
}
}
moveThread = null;
}
};
moveThread = new Thread(moveMaker);
moveThread.start();
}
}
@Deprecated
private class BlockListener implements ConnectionListener {
private StringBuffer currentBlock = new StringBuffer();
private int blockLevel = 0;
public void message(String s) {
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ('Y'&0x1F) && s.charAt(i+1) == '[') {
if(blockLevel == 0) {
currentBlock.setLength(0);
}
blockLevel++;
}
if(blockLevel > 0) {
currentBlock.append(s.charAt(i));
}
if(blockLevel > 0 && s.charAt(i) == ']' && s.charAt(i-1) == ('Y'&0x1F)) {
blockLevel--;
if(blockLevel == 0) {
for(ResponseBlock block: parseBlockResponse(currentBlock.toString())) {
for(BlockHandler handler: blockHandlers) {
handler.processBlock(block);
}
}
}
}
}
if(blockLevel > 0) {
currentBlock.append("\n");
}
}
}
private class BlockLv2Listener implements ConnectionListener {
private StringBuffer currentBlock = new StringBuffer();
public void message(String s) {
for(int i = 0; i < s.length(); i++) {
if(s.charAt(i) == ('Y'&0x1F) && s.charAt(i+1) == '(') {
currentBlock.setLength(0);
}
currentBlock.append(s.charAt(i));
if(s.charAt(i) == ')' && s.charAt(i-1) == ('Y'&0x1F)) {
for(ResponseBlockLv2 block: parseBlockResponseLv2(currentBlock.toString())) {
log.fine("LV2: " + block);
for(BlockHandler handler: blockHandlers) {
handler.processBlock(block);
}
}
}
}
currentBlock.append("\n");
}
}
@Deprecated
private List<ResponseBlock> parseBlockResponse(String s) {
List<ResponseBlock> rv = new ArrayList<ResponseBlock>();
StringBuffer buf = new StringBuffer(s);
Stack<StringBuffer> allBlocks = new Stack<StringBuffer>();
for(int i = 0; i < buf.length(); i++) {
if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == '[') {
allBlocks.push(new StringBuffer());
i++;
continue;
}
if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == ']') {
rv.add(new ResponseBlock(allBlocks.pop().toString()));
i++;
continue;
}
allBlocks.peek().append(buf.charAt(i));
}
log.fine(String.valueOf(rv));
return rv;
}
private List<ResponseBlockLv2> parseBlockResponseLv2(String s) {
List<ResponseBlockLv2> rv = new ArrayList<ResponseBlockLv2>();
StringBuffer buf = new StringBuffer(s);
Stack<StringBuffer> allBlocks = new Stack<StringBuffer>();
for(int i = 0; i < buf.length(); i++) {
if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == '(') {
allBlocks.push(new StringBuffer());
i++;
continue;
}
if(buf.charAt(i) == ('Y'&0x1F) && buf.charAt(i+1) == ')') {
rv.add(new ResponseBlockLv2(allBlocks.pop().toString()));
i++;
continue;
}
allBlocks.peek().append(buf.charAt(i));
}
log.fine(String.valueOf(rv));
return rv;
}
@Deprecated
private class ResponseBlock {
private String data;
private int code;
private String user;
private List<String> lines = new ArrayList<String>();
private ResponseBlock(String s) {
this.data = s;
StringTokenizer st = new StringTokenizer(s);
code = Integer.parseInt(st.nextToken());
user = st.nextToken();
for(st = new StringTokenizer(s, "\n"); st.hasMoreTokens(); ) {
lines.add(st.nextToken());
}
lines.remove(0); // First line is the code/user
}
public List<String> getLines() {
return lines;
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
/**
* Constructs a <code>String</code> with all attributes
* in name = value format.
*
* @return a <code>String</code> representation
* of this object.
*/
public String toString()
{
StringBuffer buf = new StringBuffer("");
buf.append("ResponseBlock(code=").append(code);
buf.append(",user=").append(user).append(")\n=============\n");
for(String s: lines) {
buf.append(s).append("\n");
}
buf.append("=============");
return buf.toString();
}
}
private class ResponseBlockLv2 {
private String data;
private int code;
private ResponseBlockLv2(String s) {
this.data = s;
StringTokenizer st = new StringTokenizer(s);
code = Integer.parseInt(st.nextToken());
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getData() {
return data;
}
public void setData(String data) {
this.data = data;
}
/**
* Constructs a <code>String</code> with all attributes
* in name = value format.
*
* @return a <code>String</code> representation
* of this object.
*/
public String toString()
{
StringBuffer buf = new StringBuffer("");
buf.append("ResponseBlock(code=").append(code).append(" = ").append(data);
return buf.toString();
}
}
}