barrysw19.calculon.fics.FICSInterface.java Source code

Java tutorial

Introduction

Here is the source code for barrysw19.calculon.fics.FICSInterface.java

Source

/**
 * 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 barrysw19.calculon.fics;

import barrysw19.calculon.engine.BitBoard;
import barrysw19.calculon.engine.ChessEngine;
import barrysw19.calculon.notation.FENUtils;
import barrysw19.calculon.notation.PGNUtils;
import barrysw19.calculon.notation.Style12;
import barrysw19.calculon.opening.OpeningBook;
import org.apache.commons.digester.Digester;
import org.apache.commons.lang.StringUtils;

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.logging.Level;
import java.util.logging.Logger;

public class FICSInterface {

    private static final Logger log = Logger.getLogger(FICSInterface.class.getName());

    private static boolean shutdown = false;
    private final static String talkResponse = "I'm sorry Dave, I'm afraid I can't do that.";
    private static FICSConfig ficsConfig;

    private Socket connection;
    private Thread moveThread = null;
    private List<ConnectionListener> listeners = new ArrayList<>();
    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 OpeningBook openingBook;
    //   private GameScorer currentScorer;
    private BitBoard currentBoard;
    private boolean blockOn = false;
    private int blockCount = 1;

    public static void main(String[] args) throws Exception {

        if (System.getProperty("calculon.password") == null) {
            log.log(Level.SEVERE, "password must be specified.");
            System.exit(-1);
        }

        while (!shutdown) {
            try {
                new FICSInterface().connect();
            } catch (Exception x) {
                log.log(Level.SEVERE, "Error", x);
                try {
                    Thread.sleep(60000);
                } catch (InterruptedException ignored) {
                }
            }
        }
    }

    private FICSInterface() {
        Digester digester = new Digester();

        digester.addObjectCreate("calculon/fics", FICSConfig.class);
        digester.addBeanPropertySetter("calculon/fics/operator-name", "operatorName");
        digester.addBeanPropertySetter("calculon/fics/login-name", "loginName");
        digester.addBeanPropertySetter("calculon/fics/accept-min", "acceptMin");
        digester.addBeanPropertySetter("calculon/fics/accept-max", "acceptMax");
        digester.addBeanPropertySetter("calculon/fics/max-rematches", "maxRematches");
        digester.addBeanPropertySetter("calculon/fics/reseek", "reseek");
        digester.addObjectCreate("calculon/fics/default-seeks/seek", FICSConfig.Seek.class);
        digester.addSetProperties("calculon/fics/default-seeks/seek", "time", "initialTime");
        digester.addSetProperties("calculon/fics/default-seeks/seek", "inc", "increment");
        digester.addSetNext("calculon/fics/default-seeks/seek", "addSeekAd");

        try {
            ficsConfig = (FICSConfig) digester.parse(ClassLoader.getSystemResourceAsStream("calculon.xml"));
        } catch (Exception e) {
            log.log(Level.WARNING, "Config reading failed", e);
            throw new RuntimeException(e);
        }
        log.fine(ficsConfig.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 GameEndedListener());
        listeners.add(new ReseekListener());
        listeners.add(new ChatListener());
        listeners.add(new BlockListener());
    }

    public void connect() throws IOException {
        connection = new Socket("freechess.org", 23);
        doLogin();
        BufferedReader reader = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        out = new PrintStream(connection.getOutputStream());

        send("set style 12");
        send("iset movecase 1");
        send("iset block 1");
        blockOn = true;

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

        Runnable keepAlive = () -> {
            while (alive) {
                send("date");
                try {
                    Thread.sleep(60000 * 15);
                } catch (InterruptedException ignored) {
                }
            }
        };
        Thread keepAliveThread = new Thread(keepAlive);
        keepAliveThread.start();

        String line;
        try {
            while ((line = reader.readLine()) != null) {
                //            if (line.trim().length() == 0) {
                //               continue;
                //            }
                log.info("Recv: '" + line + "'");
                for (ConnectionListener listener : listeners) {
                    listener.message(line);
                }
            }
        } finally {
            alive = false;
            try {
                reader.close();
                out.close();
            } catch (Exception ignored) {
            }
        }
    }

    private void doLogin() throws IOException {
        int c;
        String sLogin = "login: ";
        int sptr = 0;
        connection.getOutputStream().write("\n".getBytes());
        while ((c = connection.getInputStream().read()) != -1) {
            if (c == sLogin.charAt(sptr)) {
                sptr++;
                if (sptr == sLogin.length()) {
                    log.fine("Sending login name");
                    connection.getOutputStream().write((ficsConfig.getLoginName() + "\n").getBytes());
                    break;
                }
            } else {
                System.out.print((char) c);
                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");
                    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 < 6; i++) {
                try {
                    Thread.sleep(15000);
                } catch (InterruptedException ignored) {
                }
                if (gameNumber != -1) {
                    return;
                }
                send("resume");
            }
            for (FICSConfig.Seek seek : ficsConfig.getSeekAds()) {
                send("seek " + seek.getInitialTime() + " " + seek.getIncrement() + " formula");
            }
        };
        new Thread(seeker).start();
    }

    private synchronized void send(String s) {
        if (blockOn) {
            s = (String.valueOf(blockCount++) + " " + s);
            if (blockCount > 9) {
                blockCount = 1;
            }
        }
        log.finer(">>> " + s);
        out.println(s);
    }

    private void tellOp(String s) {
        send("tell " + ficsConfig.getOperatorName() + " " + s);
    }

    private void setStatus() {
        if (shutdown) {
            send("set 9 Current Status: Shutting down.");
        } else if (ficsConfig.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 class DebugListener implements ConnectionListener {
        public void message(String s) {
            log.finer("<<< " + s);
        }
    }

    private class ReseekListener implements ConnectionListener {
        public void message(String s) {
        }
    }

    private class ChallengeListener implements ConnectionListener {
        public void message(String s) {
            if (s.startsWith("Challenge: ") && !accept) {
                send("decline");
                return;
            }

            if (s.startsWith("Challenge: ") && accept) {
                String[] args = StringUtils.split(s);
                args[args.length - 1] = args[args.length - 1].substring(0, args[args.length - 1].length() - 1);
                int gameLength = Integer.parseInt(args[args.length - 2]) * 60
                        + Integer.parseInt(args[args.length - 1]) * 40;

                if ("rated".equals(args[args.length - 4]) && gameLength >= ficsConfig.getAcceptMin()
                        && gameLength <= ficsConfig.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 = ficsConfig.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(ficsConfig.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(ficsConfig.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;
                ficsConfig.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))) {
                    ficsConfig.setReseek(true);
                    shutdown = false;
                } else {
                    ficsConfig.setReseek(false);
                }
                tellOp("reseek " + (ficsConfig.isReseek() ? "on" : "off"));
                setStatus();
            }
        }
    }

    private class GameEndedListener implements ConnectionListener {
        private String[] PATTERNS = new String[] { " resigns}", " checkmated}", " forfeits on time}",
                " Game aborted on move ", " Neither player has mating material}", " game aborted}",
                " Game drawn by repetition}", " Game drawn by stalemate}", " Game drawn by the 50 move rule}",
                " lost connection; game adjourned}", " Game drawn because both players ran out of time}",
                " forfeits by disconnection}", " Game courtesyaborted by ", " Game aborted by mutual agreement} ",
                " has no material to mate} ", };

        public void message(String s) {
            StringBuilder buf = new StringBuilder().append("{Game ").append(gameNumber).append(" (");
            buf.append(playingWhite ? ficsConfig.getLoginName() : opponent);
            buf.append(" vs. ");
            buf.append(!playingWhite ? ficsConfig.getLoginName() : opponent);
            buf.append(") ");

            String prefix = buf.toString();
            if (!s.startsWith(prefix)) {
                return;
            }

            boolean gameEnded = false;
            for (String ending : PATTERNS) {
                gameEnded |= (s.contains(ending));
            }

            if (gameEnded) {
                log.info("Game ends: " + s);
                currentBoard = null;
                gameNumber = -1;
                opponent = null;

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

                if (shutdown) {
                    send("quit");
                } else if (ficsConfig.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();
                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 {
                    PGNUtils.applyMove(currentBoard, style12.getPreviousMovePGN());
                } catch (Exception x) {
                    log.log(Level.SEVERE, "Apply move failed", x);
                }
            }

            if (currentBoard == null || !currentBoard.getCacheId().equals(style12.getBoard().getCacheId())) {
                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;
            }

            Runnable moveMaker = () -> {
                BitBoard myBoard = currentBoard;
                ChessEngine searchNode = new ChessEngine(3);
                String bestMove = searchNode.getPreferredMove(myBoard);
                if (bestMove != null) {
                    if (gameNumber != -1) {
                        log.info("Board: " + FENUtils.generate(myBoard));
                        log.info("Moving: " + PGNUtils.translateMove(myBoard, bestMove));
                        if (currentBoard != null) {
                            PGNUtils.applyMove(currentBoard, PGNUtils.translateMove(myBoard, 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 BlockListener implements ConnectionListener {
        private StringBuffer currentBlock = new StringBuffer();
        private boolean inBlock = false;

        public void message(String s) {
            for (int i = 0; i < s.length(); i++) {
                if (s.charAt(i) == 0x15) {
                    inBlock = true;
                    currentBlock.setLength(0);
                }
                if (inBlock) {
                    currentBlock.append(s.charAt(i));
                }
                if (inBlock && s.charAt(i) == 0x17) {
                    inBlock = false;
                    new ResponseBlock(currentBlock.toString());
                }
            }
            if (inBlock) {
                currentBlock.append("\n");
            }
        }
    }

    @SuppressWarnings("unused")
    private class ResponseBlock {
        private int blockId;
        private int responseCode;
        private String data;

        private ResponseBlock(String s) {
            StringBuilder buf = new StringBuilder(s);
            if (buf.charAt(0) != 0x15) {
                throw new IllegalArgumentException("Data not started with 0x15");
            }
            if (buf.charAt(buf.length() - 1) != 0x17) {
                throw new IllegalArgumentException("Data not terminated with 0x17");
            }
            buf.delete(0, 1);
            buf.delete(buf.length() - 1, buf.length());

            blockId = Integer.parseInt(buf.substring(0, buf.indexOf("\u0016")));
            buf.delete(0, buf.indexOf("\u0016") + 1);

            responseCode = Integer.parseInt(buf.substring(0, buf.indexOf("\u0016")));
            buf.delete(0, buf.indexOf("\u0016") + 1);

            data = buf.toString();
        }

        public int getBlockId() {
            return blockId;
        }

        public void setBlockId(int blockId) {
            this.blockId = blockId;
        }

        public int getResponseCode() {
            return responseCode;
        }

        public void setResponseCode(int responseCode) {
            this.responseCode = responseCode;
        }

        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() {
            final String TAB = "    ";

            return "ResponseBlock ( " + super.toString() + TAB + "blockId = " + this.blockId + TAB
                    + "responseCode = " + this.responseCode + TAB + "data = " + this.data + TAB + " )";
        }
    }
}