Как случайным образом взаимодействовать с процессом, не останавливая качание GUI in Java? - PullRequest
2 голосов
/ 08 марта 2020

Я создаю шахматное GUI приложение, задачей которого является отображение доски и фигур и предотвращение ввода незаконных ходов.

Он также должен иметь функции, которые включают связь с шахматным движком (например, stockfi sh). Это то, с чем я сейчас борюсь. Шахматный движок - это исполняемый файл, доступ к которому осуществляется с помощью ProcessBuilder:

Process chessEngineProcess = new ProcessBuilder(chessEngineUrl).start();

InputStream processInputStream = chessEngineProcess.getInputStream();
OutputStream processOutputStream = chessEngineProcess.getOutputStream();

BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(processOutputStream));
BufferedReader reader = new BufferedReader(new InputStreamReader(processInputStream));

Я хочу отправить строки (команды в протоколе UCI) на движок, на который он отвечает, непрерывно выводя текст в течение нескольких секунд или дольше. Это зависает GUI. Мне нужно обновить textArea (в режиме реального времени) в GUI на основе выходных данных двигателя. Это не будет одноразовый тип операции. Я хотел бы сделать это случайным образом (отправить команду и обновить GUI в режиме реального времени) всякий раз, когда происходят определенные события GUI (например, пользователь делает ход).

Я знаю, что мне нужно выполнить поток читая в другой ветке, и я знаю о SwingWorker, но я просто не могу заставить его работать должным образом.

То, что я пробовал: поскольку чтение потока является блокирующей операцией (мы продолжаем ожидать вывода из механизма), поток чтения потока никогда не прекращается.

Имея это в виду, я попытался создать класс, который расширяет SwingWorker<Void, String> и устанавливает и содержит chessEngineProcess (а также его потоковое считывающее и записывающее устройство) в качестве закрытой переменной-члена. Я реализовал методы doInBackground и process. У меня также был метод publi c в этом классе для отправки команды движку.

public void sendCommandToEngine(String command) {
        try {
            writer.write(command + '\n');
            writer.flush();
        } catch (IOException e) {
            JOptionPane.showMessageDialog(null, e.getMessage());
        }
    }

Я выполняю чтение потока в doInBackground, а затем публикую sh вывод и обновляю GUI в методе process.

Это приводит к очень странному поведению, когда я отправляю движку команды из моих классов GUI (например, из прослушивателей событий). Отображаемый вывод (иногда частично, а иногда и полностью?) Неверен, и часто я получаю исключения.

Я в растерянности и очень отчаялся, поэтому, пожалуйста, помогите! Это очень важный проект. Не стесняйтесь предложить любое решение, которое, по вашему мнению, будет работать!

РЕДАКТИРОВАТЬ: Я получаю исключение нулевого указателя со следующей трассировкой стека:

Exception in thread "AWT-EventQueue-0" java.lang.NullPointerException
    at Moves.Move.isMovePossible(Move.java:84)
    at Moves.Move.executeMove(Move.java:68)
    at gui.ChessBoard.performEngineMove(ChessBoard.java:328)
    at gui.MainFrame.receiveEnginesBestMove(MainFrame.java:180)
    at gui.EngineWorker.process(EngineWorker.java:91)
    at javax.swing.SwingWorker$3.run(SwingWorker.java:414)
    at sun.swing.AccumulativeRunnable.run(AccumulativeRunnable.java:112)
    at javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.run(SwingWorker.java:832)
    at sun.swing.AccumulativeRunnable.run(AccumulativeRunnable.java:112)
    at javax.swing.SwingWorker$DoSubmitAccumulativeRunnable.actionPerformed(SwingWorker.java:842)
    at javax.swing.Timer.fireActionPerformed(Timer.java:313)
    at javax.swing.Timer$DoPostEvent.run(Timer.java:245)
    at java.awt.event.InvocationEvent.dispatch(InvocationEvent.java:311)
    at java.awt.EventQueue.dispatchEventImpl(EventQueue.java:756)
    at java.awt.EventQueue.access$500(EventQueue.java:97)
    at java.awt.EventQueue$3.run(EventQueue.java:709)
    at java.awt.EventQueue$3.run(EventQueue.java:703)
    at java.security.AccessController.doPrivileged(Native Method)
    at java.security.ProtectionDomain$JavaSecurityAccessImpl.doIntersectionPrivilege(ProtectionDomain.java:80)
    at java.awt.EventQueue.dispatchEvent(EventQueue.java:726)
    at java.awt.EventDispatchThread.pumpOneEventForFilters(EventDispatchThread.java:201)
    at java.awt.EventDispatchThread.pumpEventsForFilter(EventDispatchThread.java:116)
    at java.awt.EventDispatchThread.pumpEventsForHierarchy(EventDispatchThread.java:105)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:101)
    at java.awt.EventDispatchThread.pumpEvents(EventDispatchThread.java:93)
    at java.awt.EventDispatchThread.run(EventDispatchThread.java:82)

Некоторые детали: По сути, у меня есть класс «MainFrame», который является JFrame, который содержит все мои GUI элементы. Здесь я добавляю слушатели событий в мои компоненты. В некоторых слушателях событий я звоню sendCommandToEngine. Это запустит заблокированный doInBackground, когда механизм начнет отправлять ответы.

Затем метод process может вызвать performEnginesMove на chessBoard (который является компонентом MainFrame, отображающим шахматную доску), если он обнаруживает, что «лучший ход» был выведен двигателем.

Функция performEnginesMove проверяет, является ли ход действительным (возможным), а затем делает ход на доске (с помощью класса Move).

По какой-то причине это работает неправильно.

1 Ответ

0 голосов
/ 17 марта 2020

Я создал делегат для классов Process и ProcessBuilder, чтобы показать, как следует использовать остальную часть кода. Я называю эти классы GameEngineProcess и GameEngineProcessBuilder соответственно.

GameEngineProcess создает ответы, которые являются простыми String с и добавляются непосредственно в JTextArea GUI игрока. На самом деле он расширяет Thread, чтобы позволить ему работать асинхронно. Таким образом, реализация этого специфицированного c класса - это не то, о чем вы просите, а для имитации класса Process. Я добавил некоторую задержку в ответах этого класса, чтобы имитировать время, необходимое движку для их генерации.

Затем есть пользовательский класс OnUserActionWorker, который расширяет SwingWorker и выполняет асинхронно то, что вы запрашиваете : он получает ответы от процесса двигателя и перенаправляет их в GUI, который обновляет его JTextArea. Этот класс используется один раз для каждого запроса движка, ie мы создаем и выполняем новый экземпляр этого класса для каждого запроса, который пользователь создает при взаимодействии с GUI. Обратите внимание, что это не означает, что двигатель закрывается и вновь открывается для каждого запроса. GameEngineProcess запускается один раз, а затем остается включенным в течение всего времени безотказной игры.

Я предполагаю, что у вас есть способ сообщить, завершены ли все ответы на один запрос к движку. Для простоты в этом коде, который я написал, существует сообщение (типа String), которое пишется каждый раз в потоке процесса, чтобы указать конец ответов на запрос. Это константа END_OF_MESSAGES. Таким образом, это позволяет OnUserActionWorker знать, когда следует прекратить получение ответов, поэтому следующий экземпляр будет создаваться позднее для каждого нового запроса.

И, наконец, GUI, то есть JFrame состоящий из JTextArea и сетки кнопок, с которыми игрок может взаимодействовать и отправлять команду запроса в движок в зависимости от нажатой кнопки. Снова я использую String s в качестве команд, но я предполагаю, что это, вероятно, то, что вам также понадобится в этом случае.

Следует код:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.GridLayout;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.util.List;
import java.util.Objects;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.SwingWorker;

public class Main {

    //Just a simple 'flag' to indicate end of responses per engine request:
    private static final String END_OF_MESSAGES = "\u0000\u0000\u0000\u0000";

    //A class simulating the 'ProcessBuilder' class:
    private static class GameEngineProcessBuilder {
        private String executionCommand;

        public GameEngineProcessBuilder(final String executionCommand) {
            this.executionCommand = executionCommand;
        }

        public GameEngineProcessBuilder command(final String executionCommand) {
            this.executionCommand = executionCommand;
            return this;
        }

        public GameEngineProcess start() throws IOException {
            final GameEngineProcess gep = new GameEngineProcess(executionCommand);
            gep.setDaemon(true);
            gep.start();
            return gep;
        }
    }

    //A class simulating the 'Process' class:
    private static class GameEngineProcess extends Thread {
        private final String executionCommand; //Actually not used.
        private final PipedInputStream stdin, clientStdin;
        private final PipedOutputStream stdout, clientStdout;

        public GameEngineProcess(final String executionCommand) throws IOException {
            this.executionCommand = Objects.toString(executionCommand); //Assuming nulls allowed.

            //Client side streams:
            clientStdout = new PipedOutputStream();
            clientStdin = new PipedInputStream();

            //Remote streams (of the engine):
            stdin = new PipedInputStream(clientStdout);
            stdout = new PipedOutputStream(clientStdin);
        }

        public OutputStream getOutputStream() {
            return clientStdout;
        }

        public InputStream getInputStream() {
            return clientStdin;
        }

        @Override
        public void run() {
            try {
                final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(stdout));
                final BufferedReader br = new BufferedReader(new InputStreamReader(stdin));
                String line = br.readLine();
                while (line != null) {
                    for (int i = 0; i < 10; ++i) { //Simulate many responses per request.
                        Thread.sleep(333); //Simulate a delay in the responses.
                        bw.write(line + " (" + i + ')'); //Echo the line with the index.
                        bw.newLine();
                        bw.flush();
                    }
                    bw.write(END_OF_MESSAGES); //Indicate termination of this particular request.
                    bw.newLine();
                    bw.flush();
                    line = br.readLine();
                }
                System.out.println("Process gracefull shutdown.");
            }
            catch (final InterruptedException | IOException x) {
                System.err.println("Process termination with error: " + x);
            }
        }
    }

    //This is the SwingWorker that handles the responses from the engine and updates the GUI.
    private static class OnUserActionWorker extends SwingWorker<Void, String> {
        private final GameFrame gui;
        private final String commandToEngine;

        private OnUserActionWorker(final GameFrame gui,
                                   final String commandToEngine) {
            this.gui = Objects.requireNonNull(gui);
            this.commandToEngine = Objects.toString(commandToEngine); //Assuming nulls allowed.
        }

        //Not on the EDT...
        @Override
        protected Void doInBackground() throws Exception {
            final BufferedWriter bw = gui.getEngineProcessWriter();
            final BufferedReader br = gui.getEngineProcessReader();

            //Send request:
            bw.write(commandToEngine);
            bw.newLine();
            bw.flush();

            //Receive responses:
            String line = br.readLine();
            while (line != null && !line.equals(END_OF_MESSAGES)) {
                publish(line); //Use 'publish' to forward the text to the 'process' method.
                line = br.readLine();
            }

            return null;
        }

        //On the EDT...
        @Override
        protected void done() {
            gui.responseDone(); //Indicate end of responses at the GUI level.
        }

        //On the EDT...
        @Override
        protected void process(final List<String> chunks) {
            chunks.forEach(chunk -> gui.responsePart(chunk)); //Sets the text of the the text area of the GUI.
        }
    }

    //The main frame of the GUI of the user/player:
    private static class GameFrame extends JFrame implements Runnable {
        private final JButton[][] grid;
        private final JTextArea output;
        private BufferedReader procReader;
        private BufferedWriter procWriter;

        public GameFrame(final int rows,
                         final int cols) {
            super("Chess with remote engine");

            output = new JTextArea(rows, cols);
            output.setEditable(false);
            output.setFont(new Font(Font.MONOSPACED, Font.ITALIC, output.getFont().getSize()));

            final JPanel gridPanel = new JPanel(new GridLayout(0, cols));

            grid = new JButton[rows][cols];
            for (int row = 0; row < rows; ++row)
                for (int col = 0; col < cols; ++col) {
                    final JButton b = new JButton(String.format("Chessman %02d,%02d", row, col));
                    b.setPreferredSize(new Dimension(b.getPreferredSize().width, 50));
                    b.addActionListener(e -> sendCommandToEngine("Click \"" + b.getText() + "\"!"));
                    gridPanel.add(b);
                    grid[row][col] = b;
                }

            final JScrollPane outputScroll = new JScrollPane(output);
            outputScroll.setPreferredSize(gridPanel.getPreferredSize());

            final JPanel contents = new JPanel(new BorderLayout());
            contents.add(gridPanel, BorderLayout.LINE_START);
            contents.add(outputScroll, BorderLayout.CENTER);

            super.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            super.getContentPane().add(contents);
            super.pack();
        }

        //Utility method to enable/disable all the buttons of the grid at once:
        private void gridSetEnabled(final boolean enabled) {
            for (final JButton[] row: grid)
                for (final JButton b: row)
                    b.setEnabled(enabled);
        }

        //This is the method that sends the next request to the engine:
        private void sendCommandToEngine(final String commandToEngine) {
            gridSetEnabled(false);
            output.setText("> Command accepted.");
            new OnUserActionWorker(this, commandToEngine).execute();
        }

        public BufferedReader getEngineProcessReader() {
            return procReader;
        }

        public BufferedWriter getEngineProcessWriter() {
            return procWriter;
        }

        //Called by 'SwingWorker.process':
        public void responsePart(final String msg) {
            output.append("\n" + msg);
        }

        //Called by 'SwingWorker.done':
        public void responseDone() {
            output.append("\n> Response finished.");
            gridSetEnabled(true);
        }

        @Override
        public void run() {
            try {
                //Here you build and start the process:
                final GameEngineProcess proc = new GameEngineProcessBuilder("stockfish").start();

                //Here you obtain the I/O streams:
                procWriter = new BufferedWriter(new OutputStreamWriter(proc.getOutputStream()));
                procReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));

                //Finally show the GUI:
                setLocationRelativeTo(null);
                setVisible(true);
            }
            catch (final IOException iox) {
                JOptionPane.showMessageDialog(null, iox.toString());
            }
        }
    }

    public static void main(final String[] args) {
        new GameFrame(3, 3).run(); //The main thread starts the game, which shows the GUI...
    }
}

И, наконец, Другое важное предположение, которое я сделал, заключается в том, что когда пользователь взаимодействует с GUI, GUI блокирует ввод (но продолжает реагировать на другие события). Это препятствует тому, чтобы пользователь имел больше чем один активный запрос к двигателю одновременно. Под блокирующим вводом я просто подразумеваю, что при нажатии кнопки сначала все кнопки отключаются, а затем команда отправляется в движок. После этого все кнопки снова включаются, когда все ответы на последний сделанный запрос завершаются sh.

Если вам нужно одновременно выполнить несколько запросов к одному движку, вам, вероятно, потребуется синхронизируйте доступ к некоторым методам GUI, а также убедитесь, что каждый OnUserActionWorker может отличать guish своих ответов от других. Так что это была бы другая история, но дайте мне знать, если вы этого хотите.

Чтобы проверить отзывчивость EDT во время получения ответов, вы можете, например, просто изменить размер окна с помощью мыши. пока (десять) ответов все еще принимаются, или просто обратите внимание, что ответы распечатываются в JTextArea в режиме реального времени.

Надеюсь, это поможет.

...