我正在构建一个国际象棋GUI应用程序,其作用是显示棋盘和棋子,并防止输入非法举动。
它还应具有涉及与国际象棋引擎(例如鳕鱼)通信的功能。这就是我现在正在努力的目标。国际象棋引擎是一个exe文件,可以使用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。我需要基于引擎的输出在GUI中实时更新textArea。这不是一次性的操作。每当某些GUI事件发生时(例如用户进行移动),我都希望随机执行此操作(发送命令并实时更新GUI)。
我知道我需要在另一个线程中读取流,并且我了解SwingWorker,但是我无法使其正常工作。
我尝试过:由于流读取是一项阻塞操作(我们一直在等待来自引擎的输出),因此流读取线程永远不会终止。
考虑到这一点,我尝试创建一个扩展SwingWorker<Void, String>
并设置并包含chessEngineProcess
(及其流读取器和写入器)作为私有成员变量的类。我实现了doInBackground
和process
方法。我在此类中也有一个用于向引擎发送命令的公共方法。
public void sendCommandToEngine(String command) {
try {
writer.write(command + '\n');
writer.flush();
} catch (IOException e) {
JOptionPane.showMessageDialog(null, e.getMessage());
}
}
我在doInBackground
中读取流,然后以process
方法发布输出并更新GUI。
当我从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”类,它是一个包含所有GUI元素的JFrame。这是我将事件侦听器添加到组件的地方。在某些事件侦听器中,我称为sendCommandToEngine
。当引擎开始发送响应时,这将启动被阻止的doInBackground
。
process
方法可以在检测到引擎输出了“最佳动作”时,在performEnginesMove
(这是显示棋盘的MainFrame组件)上调用chessBoard
。
performEnginesMove
函数检查移动是否有效(可能),然后在板上进行移动(借助于Move类)。
由于某种原因,这行不通。
我为Process
和ProcessBuilder
类构建了一个委托,以显示应如何使用其余代码。我分别将这些类称为GameEngineProcess
和GameEngineProcessBuilder
。
GameEngineProcess
正在创建响应,这些响应是简单的String
,可以直接附加到播放器GUI的JTextArea
中。实际上,它扩展了Thread
以使其异步运行。因此,该特定类的实现不是您要的,而是用于模拟Process
类。我在此类的响应中添加了一些延迟,以模拟引擎生成它们所需的时间。
然后有一个自定义类OnUserActionWorker
,它扩展了SwingWorker
并异步执行您所要的操作:它接收来自引擎进程的响应,并将其转发给更新其JTextArea
的GUI。该类在每个引擎请求中使用一次,即,我们为用户在与GUI交互时创建的每个请求创建并执行该类的新实例。请注意,这并不意味着引擎会针对每个请求关闭并重新打开。 GameEngineProcess
一次启动,然后在整个游戏正常运行时间内保持运行。
我假设您有一种方法可以判断单个引擎请求是否已完成所有响应。为了简化起见,我编写的这段代码中存在一条消息(类型为String
),该消息每次都会在流程流中写入,以指示每个请求的响应结束。这是END_OF_MESSAGES
常数。因此,这使OnUserActionWorker
知道何时终止接收响应,因此稍后将为每个新请求创建它的下一个实例。
[最后是GUI,它是JFrame
,由JTextArea
和按钮网格组成,玩家可以根据按下的按钮与之交互并向引擎发送请求命令。再次,我使用String
作为命令,但我猜想这可能也是您在这种情况下需要的。
遵循代码:
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会阻止input(但会继续响应其他事件)。这样可以防止用户同时对引擎有多个活动请求。通过blocking input我的意思是简单地说,当您单击按钮时,首先禁用所有按钮,然后将命令发送到引擎。当对最新发出的请求的所有响应都完成时,按钮将全部重新启用。
如果您需要同时对一个引擎有多个请求,那么您可能需要同步某些GUI方法的访问,并确保每个OnUserActionWorker
都可以将其响应与其他。所以那将是一个不同的故事,但是让我知道这是否是您想要的。
要在接收响应时测试EDT的响应性,您可以例如在仍接收(十个)响应时用鼠标简单调整窗口的大小,或者只是注意将响应打印到JTextArea
中实时。
希望有帮助。