barrysw19.calculon.icc.ICCInterface.java Source code

Java tutorial

Introduction

Here is the source code for barrysw19.calculon.icc.ICCInterface.java

Source

/**
 * Calculon - A Java chess-engine.
 * <p/>
 * Copyright (C) 2008-2013 Barry Smith
 * <p/>
 * 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
 * <p/>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p/>
 * 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 barrysw19.calculon.icc;

import barrysw19.calculon.notation.PGNUtils;
import barrysw19.calculon.engine.BitBoard;
import barrysw19.calculon.engine.ChessEngine;
import barrysw19.calculon.engine.ClockStatus;
import barrysw19.calculon.engine.MoveGeneratorImpl;
import barrysw19.calculon.model.Piece;
import barrysw19.calculon.notation.FENUtils;
import barrysw19.calculon.notation.Style12;
import barrysw19.calculon.opening.OpeningBook;
import org.apache.commons.digester.Digester;
import org.apache.commons.lang.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.*;
import java.net.Socket;
import java.util.*;

public class ICCInterface {
    private static final Logger LOG = LoggerFactory.getLogger(ICCInterface.class);

    private static final String TALK_RESPONSE = "I'm sorry Dave, I'm afraid I can't do that.";

    private static boolean shutdown = false;
    private static ICCSConfig iccConfig;

    private Socket connection;
    private Thread moveThread = null;
    private List<ConnectionListener> listeners = new ArrayList<>();
    private List<BlockHandler> blockHandlers = new ArrayList<>();
    private PrintStream out;
    private String opponent = null;
    private boolean rated = true;
    private boolean accept = true;
    private boolean alive = true;
    private BitBoard currentBoard;
    private OpeningBook openingBook;
    private Map<Byte, ClockStatus> clocks = new HashMap<>();
    private Lv1BlockHandler lv1BlockHandler = new Lv1BlockHandler(this);

    private volatile int gameNumber = -1;

    public static void main(String[] args) throws Exception {
        if (System.getProperty("calculon.password") == null) {
            LOG.error("password must be specified.");
            System.exit(-1);
        }

        while (!shutdown) {
            try {
                new ICCInterface().connect();
            } catch (Exception x) {
                LOG.error("Error", x);
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException ix) {
                    LOG.error("This doesn't happen", 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.addBeanPropertySetter("calculon/icc/formula", "formula");
        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.warn("Config reading failed", e);
            throw new RuntimeException(e);
        }
        LOG.trace(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 BlockLv2Listener());

        blockHandlers.add(new GameStartedHandler());
        blockHandlers.add(new GameEndedHandler());
        blockHandlers.add(new ClockUpdateHandler());
    }

    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");
        if (!StringUtils.isBlank(iccConfig.getFormula())) {
            send("set formula " + iccConfig.getFormula());
        }
        receiveLevel2(DgCommand.DG_MY_GAME_STARTED, DgCommand.DG_MY_GAME_RESULT, DgCommand.DG_SEND_MOVES,
                DgCommand.DG_MOVE_ALGEBRAIC, DgCommand.DG_MOVE_SMITH, DgCommand.DG_MOVE_TIME,
                DgCommand.DG_MOVE_CLOCK, DgCommand.DG_MSEC);

        setStatus();
        if (iccConfig.isReseek()) {
            reseek();
        }

        Runnable keepAlive = () -> {
            while (alive) {
                send("games *r-e-B-o-M-f-K-w-L-d-z");
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException x) {
                    // Ignore
                }
            }
        };
        Thread keepAliveThread = new Thread(keepAlive);
        keepAliveThread.setDaemon(true);
        keepAliveThread.start();

        StringBuilder line = new StringBuilder();
        int c;
        try {
            while ((c = reader.read()) != -1) {
                lv1BlockHandler.add((char) c);

                // Ignore CTRL-M, CTRL-G
                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) {
                // ignore
            }
        }
    }

    private void receiveLevel2(DgCommand... dgCommands) {
        for (DgCommand dgCommand : dgCommands) {
            send("set-2 " + String.valueOf(dgCommand.getCommandNumber()) + " 1");
        }
    }

    private void fireDataReceived(String s) {
        for (ConnectionListener listener : listeners) {
            try {
                listener.message(s);
            } catch (Exception e) {
                LOG.warn("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.debug("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.debug("Sending password");
                    connection.getOutputStream().write((System.getProperty("calculon.password") + "\n").getBytes());
                    break;
                }
            } else {
                sptr = 0;
            }
        }
    }

    private void reseek() {
        send("resume");
        Runnable seeker = () -> {
            for (int i = 0; i < 4; i++) {
                try {
                    Thread.sleep(15000);
                } catch (InterruptedException x) {
                    // ignore
                }
                if (gameNumber != -1) {
                    return;
                }
                send("resume");
            }
            for (ICCSConfig.Seek seek : iccConfig.getSeekAds()) {
                send(seek.getCommand());
            }
        };
        new Thread(seeker).start();
    }

    public synchronized void send(String s) {
        LOG.debug(">>> {}", 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 {
        void message(String s);
    }

    private interface BlockHandler {
        void processBlock(ResponseBlockLv2 responseBlock);

        boolean accept(ResponseBlockLv2 responseBlock);
    }

    private class DebugListener implements ConnectionListener {
        public void message(String s) {
            LOG.debug("<<< {}", 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.debug("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.debug("Accepting: '{}' {}s", s, gameLength);
                    send("accept");
                } else {
                    LOG.debug("Rejecting: '{}' {}s", s, gameLength);
                    send("decline");
                }
                return;
            }

            if (s.startsWith("Creating: ")) {
                LOG.info("Starting game: '" + s + "'");
                List<String> fields = Arrays.asList(StringUtils.split(s));
                boolean playingWhite = iccConfig.getLoginName().equals(fields.get(1));
                opponent = playingWhite ? fields.get(3) : fields.get(1);
                rated = "rated".equals(fields.get(5));
                send("finger " + opponent);
            }
        }
    }

    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] + " " + TALK_RESPONSE);
                saveChat(s);
            }
            if (fields.length >= 3 && "says:".equals(fields[1])) {
                send("say " + TALK_RESPONSE);
                saveChat(s);
            }
        }
    }

    private static void saveChat(String s) {
        try {
            PrintWriter pw = new PrintWriter(new FileWriter("c:/Development/chatlog.txt", true));
            pw.println(s);
            pw.close();
        } catch (IOException e) {
            LOG.error("Error writing chatlog", e);
        }
    }

    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))) {
                StringBuilder buf = new StringBuilder();
                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();
            }
        }
    }

    /**
     * This method listens for board status messages broadcast when each move is made, or a game starts,
     * and makes a move when it's my turn.
     */
    private class BoardListener implements ConnectionListener {
        public void message(String s) {
            if (!s.startsWith("<12> ")) {
                return;
            }

            final Style12 style12 = new Style12(s);
            if (!style12.isMyGame()) {
                return;
            }

            gameNumber = style12.getGameNumber();
            opponent = style12.getOpponentName();
            if (style12.isInitialPosition()) {
                LOG.debug("Creating new board for new game");
                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 {
                    String applyMove = PGNUtils.toPgnMoveMap(currentBoard).get(style12.getPreviousMovePGN());
                    if (applyMove != null) {
                        currentBoard.makeMove(currentBoard.getMove(applyMove));
                    } else {
                        LOG.warn(String.format("Out of sync board: move %s not possible: %s",
                                style12.getPreviousMovePGN(), FENUtils.generate(currentBoard)));
                        currentBoard = style12.getBoard();
                    }
                } catch (Exception x) {
                    LOG.error("Apply move failed", x);
                }
            }

            if (currentBoard == null || !currentBoard.equalPosition(style12.getBoard())) {
                LOG.warn("Out of sync board detected - resetting! "
                        + (currentBoard == null ? "" : FENUtils.generate(currentBoard)) + " ||| "
                        + FENUtils.generate(style12.getBoard()));
                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.debug("Using book move: " + bookMove);
                return;
            }

            if (!new MoveGeneratorImpl(currentBoard).hasNext()) {
                LOG.debug("Looks like that game is over!");
                return;
            }

            Runnable moveMaker = () -> {
                BitBoard myBoard = currentBoard;
                ChessEngine engine = new ChessEngine();
                myBoard.getPlayer();
                ClockStatus clockStatus = clocks.get(myBoard.getPlayer());
                if (clockStatus != null) {
                    int moveTime = clockStatus.getSecondsForMoves(20) / 20;
                    int maxNow = (int) (clockStatus.getMsec() / 1000);
                    moveTime = Math.min(moveTime, maxNow);
                    engine.setTargetTime(Math.max(1, moveTime));
                    LOG.info("Set clock " + moveTime);
                } else {
                    LOG.error("No clock status");
                }
                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();
        }
    }

    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.debug("LV2: {}", block);
                        blockHandlers.stream().filter(handler -> handler.accept(block))
                                .forEach(handler -> handler.processBlock(block));
                    }
                }
            }
            currentBlock.append("\n");
        }
    }

    private List<ResponseBlockLv2> parseBlockResponseLv2(String lvl2String) {
        List<ResponseBlockLv2> rv = new ArrayList<>();

        Stack<StringBuffer> allBlocks = new Stack<>();
        for (int i = 0; i < lvl2String.length(); i++) {
            if (lvl2String.charAt(i) == ('Y' & 0x1F) && lvl2String.charAt(i + 1) == '(') {
                allBlocks.push(new StringBuffer());
                i++;
                continue;
            }
            if (lvl2String.charAt(i) == ('Y' & 0x1F) && lvl2String.charAt(i + 1) == ')') {
                rv.add(ResponseBlockLv2.createResponseBlock(allBlocks.pop().toString()));
                i++;
                continue;
            }
            allBlocks.peek().append(lvl2String.charAt(i));
        }

        LOG.debug(String.valueOf(rv));
        return rv;
    }

    // Level 2 style block handlers

    private class ClockUpdateHandler implements BlockHandler {

        @Override
        public boolean accept(ResponseBlockLv2 responseBlock) {
            return responseBlock.getCommand() == DgCommand.DG_MSEC
                    && Integer.parseInt(responseBlock.tokenize()[1]) == gameNumber;
        }

        @Override
        public void processBlock(ResponseBlockLv2 responseBlock) {
            Lv2TimeUpdate timeUpdate = new Lv2TimeUpdate(responseBlock);
            ClockStatus clockStatus = clocks.get(timeUpdate.getColor());
            clockStatus.setMsec(timeUpdate.getMsec());
            LOG.debug("Clock Set: " + clockStatus);
        }
    }

    private class GameStartedHandler implements BlockHandler {

        @Override
        public boolean accept(ResponseBlockLv2 responseBlock) {
            return responseBlock.getCommand() == DgCommand.DG_MY_GAME_STARTED;
        }

        @Override
        public void processBlock(ResponseBlockLv2 responseBlock) {
            Lv2MyGameStarted lv2MyGameStarted = new Lv2MyGameStarted(responseBlock);
            clocks.put(Piece.WHITE, new ClockStatus());
            clocks.put(Piece.BLACK, new ClockStatus());

            clocks.get(Piece.WHITE).setInitialMinutes(lv2MyGameStarted.getInitialTime(Piece.WHITE));
            clocks.get(Piece.WHITE).setSecondIncrement(lv2MyGameStarted.getIncrement(Piece.WHITE));
            clocks.get(Piece.BLACK).setInitialMinutes(lv2MyGameStarted.getInitialTime(Piece.BLACK));
            clocks.get(Piece.BLACK).setSecondIncrement(lv2MyGameStarted.getIncrement(Piece.BLACK));
            LOG.info("Game starts:" + clocks);
        }
    }

    private class GameEndedHandler implements BlockHandler {

        @Override
        public boolean accept(ResponseBlockLv2 responseBlock) {
            return responseBlock.getCommand() == DgCommand.DG_MY_GAME_RESULT;
        }

        @Override
        public void processBlock(ResponseBlockLv2 responseBlock) {
            LOG.trace("Game ends: [" + responseBlock.getData() + "]");
            currentBoard = null;
            gameNumber = -1;
            opponent = null;

            while (moveThread != null && moveThread.isAlive()) {
                try {
                    Thread.sleep(200);
                } catch (InterruptedException x) {
                    // Ignore
                }
            }

            if (shutdown) {
                send("quit");
            } else if (iccConfig.isReseek()) {
                reseek();
            }
        }
    }
}