Local tic tac toe thread

This commit is contained in:
lieght
2025-09-19 17:31:37 +02:00
parent 1d90df4e86
commit ba16f2e135
38 changed files with 327 additions and 236 deletions

View File

@@ -0,0 +1,108 @@
package org.toop.frontend;
import org.toop.eventbus.Events;
import org.toop.eventbus.GlobalEventBus;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
public class ConnectionManager {
private static final Logger logger = LogManager.getLogger(ConnectionManager.class);
/**
* Map of serverId -> Server instances
*/
private final Map<String, ServerConnection> serverConnections = new ConcurrentHashMap<>();
/**
* Starts a connection manager, to manage, connections.
*/
public ConnectionManager() {
GlobalEventBus.subscribeAndRegister(Events.ServerEvents.StartConnectionRequest.class, this::handleStartConnectionRequest);
GlobalEventBus.subscribeAndRegister(Events.ServerEvents.StartConnection.class, this::handleStartConnection);
GlobalEventBus.subscribeAndRegister(Events.ServerEvents.SendCommand.class, this::handleCommand);
GlobalEventBus.subscribeAndRegister(Events.ServerEvents.Reconnect.class, this::handleReconnect);
// GlobalEventBus.subscribeAndRegister(Events.ServerEvents.ChangeConnection.class, this::handleChangeConnection);
GlobalEventBus.subscribeAndRegister(Events.ServerEvents.ForceCloseAllConnections.class, _ -> shutdownAll());
GlobalEventBus.subscribeAndRegister(Events.ServerEvents.RequestsAllConnections.class, this::getAllConnections);
}
private String startConnectionRequest(String ip, String port) {
String connectionId = UUID.randomUUID().toString();
try {
ServerConnection connection = new ServerConnection(connectionId, ip, port);
this.serverConnections.put(connectionId, connection);
new Thread(connection, "Connection-" + connectionId).start();
logger.info("Connected to server {} at {}:{}", connectionId, ip, port);
} catch (IOException e) {
logger.error("{}", e);
}
return connectionId;
}
private void handleStartConnectionRequest(Events.ServerEvents.StartConnectionRequest request) {
request.future().complete(this.startConnectionRequest(request.ip(), request.port())); // TODO: Maybe post ConnectionEstablished event.
}
private void handleStartConnection(Events.ServerEvents.StartConnection event) {
GlobalEventBus.post(new Events.ServerEvents.ConnectionEstablished(
this.startConnectionRequest(event.ip(), event.port()),
event.ip(),
event.port()
));
}
private void handleCommand(Events.ServerEvents.SendCommand event) { // TODO: Move this to ServerConnection class, keep it internal.
ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
if (serverConnection != null) {
serverConnection.sendCommandByString(event.args());
} else {
logger.warn("Server {} not found for command '{}'", event.connectionId(), event.args());
}
}
private void handleReconnect(Events.ServerEvents.Reconnect event) {
ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
if (serverConnection != null) {
try {
serverConnection.reconnect();
logger.info("Server {} reconnected", event.connectionId());
} catch (Exception e) {
logger.error("Server {} failed to reconnect", event.connectionId(), e);
GlobalEventBus.post(new Events.ServerEvents.CouldNotConnect(event.connectionId()));
}
}
}
// private void handleChangeConnection(Events.ServerEvents.ChangeConnection event) {
// ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
// if (serverConnection != null) {
// try {
// serverConnection.connect(event.ip(), event.port());
// logger.info("Server {} changed connection to {}:{}", event.connectionId(), event.ip(), event.port());
// } catch (Exception e) {
// logger.error("Server {} failed to change connection", event.connectionId(), e);
// GlobalEventBus.post(new Events.ServerEvents.CouldNotConnect(event.connectionId()));
// }
// }
// } TODO
private void getAllConnections(Events.ServerEvents.RequestsAllConnections request) {
List<ServerConnection> a = new ArrayList<>(this.serverConnections.values());
request.future().complete(a.toString());
}
public void shutdownAll() {
this.serverConnections.values().forEach(ServerConnection::closeConnection);
this.serverConnections.clear();
logger.info("All servers shut down");
}
}

View File

@@ -0,0 +1,172 @@
package org.toop.frontend;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.eventbus.Events;
import org.toop.eventbus.GlobalEventBus;
import java.io.IOException;
import java.net.InetAddress;
import java.util.concurrent.*;
public final class ServerConnection extends TcpClient implements Runnable {
private static final Logger logger = LogManager.getLogger(ServerConnection.class);
private final BlockingQueue<String> receivedQueue = new LinkedBlockingQueue<>();
private final BlockingQueue<String> sendQueue = new LinkedBlockingQueue<>();
private final ExecutorService executor = Executors.newFixedThreadPool(2);
String uuid;
volatile boolean running = false;
public ServerConnection(String uuid, String ip, String port) throws IOException {
super(ip, Integer.parseInt(port)); // TODO: Verify if port is integer first, to avoid crash.
this.uuid = uuid;
this.initEvents();
}
/**
*
* Sends a command to the server.
*
* @param args The arguments for the command.
*/
public void sendCommandByString(String... args) {
// if (!TicTacToeServerCommand.isValid(command)) {
// logger.error("Invalid command: {}", command);
// return;
// } // TODO: DO I CARE?
// if (!this.running) {
// logger.warn("Server has been stopped");
// return;
// } // TODO: Server not running
String command = String.join(" ", args);
this.sendQueue.add(command);
logger.info("Command '{}' added to the queue", command); // TODO: Better log, which uuid?
}
private void addReceivedMessageToQueue(String message) {
try {
receivedQueue.put(message);
} catch (InterruptedException e) {
logger.error("{}", e); // TODO: Make more informative
}
}
private void initEvents() {}
private void startWorkers() {
running = true;
this.executor.submit(this::inputLoop);
this.executor.submit(this::outputLoop);
}
private void stopWorkers() {
this.running = false;
this.sendQueue.clear();
try {
this.closeSocket();
} catch (IOException e) {
logger.warn("Error closing client socket", e); // TODO: Better log
}
this.executor.shutdownNow();
}
private void inputLoop() {
logger.info("Starting {}:{} connection read", this.serverAddress, this.serverPort);
try {
while (running) {
String received = this.readLine(); // blocks
if (received != null) {
logger.info("Connection: {} received: '{}'", this.uuid, received);
// this.addReceivedMessageToQueue(received); // TODO: Will never go empty
GlobalEventBus.post(new Events.ServerEvents.ReceivedMessage(this.uuid, received)); // TODO: mb change
} else {
break;
}
}
} catch (IOException e) {
logger.error("Error reading from server", e);
}
}
private void outputLoop() {
logger.info("Starting {}:{} connection write", this.serverAddress, this.serverPort);
try {
while (this.running) {
String command = this.sendQueue.poll(500, TimeUnit.MILLISECONDS);
if (command != null) {
this.sendMessage(command);
logger.info("Sent command: '{}'", command);
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (IOException e) {
logger.error("Error sending command", e);
}
}
/**
*
* Connect to a new server.
*
* @param ip The ip to connect to.
* @param port The port to connect to.
*/
public void connect(InetAddress ip, int port) {
if (this.running) {
this.closeConnection(); // Also stops workers.
}
this.serverAddress = ip;
this.serverPort = port;
this.startWorkers();
}
/**
*
* Reconnects to previous address.
*
* @throws IOException wip
*/
public void reconnect() throws IOException {
this.connect(this.serverAddress, this.serverPort);
}
/**
*
* Close connection to server.
*
*/
public void closeConnection() {
this.stopWorkers();
logger.info("Closed connection: {}, to server {}:{}", this.uuid, this.serverAddress, this.serverPort);
}
@Override
public void run() {
try {
reconnect();
} catch (IOException e) {
logger.error("Initial connection failed", e);
}
}
@Override
public String toString() {
return String.format(
"Server {ip: \"%s\", port: \"%s\", running: %s}",
this.serverAddress, this.serverPort, this.running
);
}
}

View File

@@ -0,0 +1,65 @@
package org.toop.frontend;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
/**
* A simple wrapper for creating TCP clients.
*/
public abstract class TcpClient {
InetAddress serverAddress;
int serverPort;
Socket socket;
BufferedReader in;
PrintWriter out;
public TcpClient(byte[] serverIp, int serverPort) throws IOException {
this.serverAddress = InetAddress.getByAddress(serverIp);
this.serverPort = serverPort;
this.socket = createSocket();
this.in = createIn();
this.out = createOut();
}
public TcpClient(String serverIp, int serverPort) throws IOException {
this.serverAddress = InetAddress.getByName(serverIp);
this.serverPort = serverPort;
this.socket = createSocket();
this.in = createIn();
this.out = createOut();
}
public Socket createSocket() throws IOException {
return new Socket(serverAddress, serverPort);
}
public void closeSocket() throws IOException {
this.socket.close();
}
private BufferedReader createIn() throws IOException {
return new BufferedReader(new InputStreamReader(this.socket.getInputStream()));
}
private PrintWriter createOut() throws IOException {
return new PrintWriter(this.socket.getOutputStream(), true);
}
public void sendMessage(String message) throws IOException {
this.out.println(message);
}
public String readLine() throws IOException {
return this.in.readLine();
}
public void close() throws IOException {
this.socket.close();
}
}

View File

@@ -0,0 +1,21 @@
package org.toop.frontend.UI;
import javax.swing.*;
import java.awt.*;
public class BackgroundPanel extends JPanel {
private Image backgroundImage;
public void setBackgroundImage(Image image) {
this.backgroundImage = image;
repaint();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (backgroundImage != null) {
g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), this);
}
}
}

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.toop.frontend.UI.LocalGameSelector">
<grid id="27dc6" binding="panel1" default-binding="true" layout-manager="GridLayoutManager" row-count="3" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="780" height="600"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="3f7f6" class="javax.swing.JComboBox" binding="gameSelectionComboBox">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<model/>
</properties>
</component>
<component id="4700f" class="javax.swing.JButton" binding="startGame">
<constraints>
<grid row="2" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Start game"/>
</properties>
</component>
<component id="41f79" class="javax.swing.JComboBox" binding="playerTypeSelectionBox">
<constraints>
<grid row="1" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<editable value="false"/>
</properties>
</component>
</children>
</grid>
</form>

View File

@@ -0,0 +1,83 @@
package org.toop.frontend.UI;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.eventbus.Events;
import org.toop.eventbus.GlobalEventBus;
import org.toop.frontend.games.LocalTicTacToe;
import javax.swing.*;
import java.awt.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
public class LocalGameSelector extends JFrame {
private static final Logger logger = LogManager.getLogger(LocalGameSelector.class);
private JPanel panel1;
private JComboBox gameSelectionComboBox;
private JButton startGame;
private JComboBox playerTypeSelectionBox;
private JPanel cards; // CardLayout panel
private CardLayout cardLayout;
private UIGameBoard tttBoard;
public LocalGameSelector() {
setTitle("Local Game Selector");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(1920, 1080);
setLocationRelativeTo(null);
// Setup CardLayout
cardLayout = new CardLayout();
cards = new JPanel(cardLayout);
setContentPane(cards);
// --- Main menu panel ---
panel1 = new JPanel();
panel1.setLayout(new FlowLayout());
gameSelectionComboBox = new JComboBox<>();
gameSelectionComboBox.addItem("Tic Tac Toe");
gameSelectionComboBox.addItem("Reversi");
playerTypeSelectionBox = new JComboBox<>();
playerTypeSelectionBox.addItem("Player vs Player");
playerTypeSelectionBox.addItem("Player vs AI");
playerTypeSelectionBox.addItem("AI vs Player");
panel1.add(gameSelectionComboBox);
panel1.add(playerTypeSelectionBox);
startGame = new JButton("Start Game");
panel1.add(startGame);
cards.add(panel1, "MainMenu");
// Start button action
startGame.addActionListener(e -> startGameClicked());
setVisible(true);
}
private void startGameClicked() {
String playerTypes = (String) playerTypeSelectionBox.getSelectedItem();
String selectedGame = (String) gameSelectionComboBox.getSelectedItem();
LocalTicTacToe lttt = new LocalTicTacToe(true, "127.0.0.1", "5001");
if ("Tic Tac Toe".equalsIgnoreCase(selectedGame)) {
if (tttBoard == null) {
tttBoard = new UIGameBoard(lttt, this);
cards.add(tttBoard.getTTTPanel(), "TicTacToe");
}
cardLayout.show(cards, "TicTacToe");
}
}
public void showMainMenu() {
cardLayout.show(cards, "MainMenu");
}
}

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.toop.frontend.UI.LocalServerSelector">
<grid id="27dc6" binding="panel1" default-binding="true" layout-manager="GridLayoutManager" row-count="1" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children>
<component id="7774a" class="javax.swing.JButton" binding="serverButton" default-binding="true">
<constraints>
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Server"/>
</properties>
</component>
<component id="ca0c8" class="javax.swing.JButton" binding="localButton">
<constraints>
<grid row="0" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Local"/>
</properties>
</component>
<hspacer id="74dd2">
<constraints>
<grid row="0" column="3" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<hspacer id="50fd">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
</children>
</grid>
</form>

View File

@@ -0,0 +1,34 @@
package org.toop.frontend.UI;
import javax.swing.*;
public class LocalServerSelector {
private JPanel panel1;
private JButton serverButton;
private JButton localButton;
private final JFrame frame;
public LocalServerSelector() {
frame = new JFrame("Server Selector");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setContentPane(panel1);
frame.setSize(1920, 1080);
frame.setLocationRelativeTo(null); // Sets to center
frame.setVisible(true);
serverButton.addActionListener(e -> onServerClicked());
localButton.addActionListener(e -> onLocalClicked());
}
private void onServerClicked() {
frame.dispose();
new RemoteGameSelector();
}
private void onLocalClicked() {
frame.dispose();
new LocalGameSelector();
}
}

View File

@@ -0,0 +1,147 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.toop.frontend.UI.RemoteGameSelector">
<grid id="27dc6" binding="mainMenu" layout-manager="GridLayoutManager" row-count="10" column-count="4" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="10" left="10" bottom="10" right="10"/>
<constraints>
<xy x="20" y="20" width="741" height="625"/>
</constraints>
<properties>
<opaque value="true"/>
<preferredSize width="800" height="600"/>
</properties>
<border type="none"/>
<children>
<component id="ed019" class="javax.swing.JTextField" binding="nameTextField">
<constraints>
<grid row="2" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<text value=""/>
</properties>
</component>
<vspacer id="4e05c">
<constraints>
<grid row="9" column="2" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
<component id="1d2f1" class="javax.swing.JLabel">
<constraints>
<grid row="2" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Name:"/>
</properties>
</component>
<component id="70d8c" class="javax.swing.JLabel">
<constraints>
<grid row="1" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Welcome to ISY games selector!"/>
</properties>
</component>
<component id="5cc82" class="javax.swing.JTextField" binding="name2TextField">
<constraints>
<grid row="3" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties>
<text value=""/>
</properties>
</component>
<hspacer id="27f2a">
<constraints>
<grid row="3" column="3" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<component id="3874e" class="javax.swing.JLabel" binding="fillAllFields">
<constraints>
<grid row="8" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<enabled value="true"/>
<foreground color="-65536"/>
<text value="Please fill all fields"/>
<visible value="false"/>
</properties>
</component>
<component id="a2f0" class="javax.swing.JButton" binding="connectButton" default-binding="true">
<constraints>
<grid row="7" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="3" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Connect"/>
</properties>
</component>
<component id="6a4c6" class="javax.swing.JTextField" binding="ipTextField">
<constraints>
<grid row="4" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="4214e" class="javax.swing.JComboBox" binding="gameSelectorBox">
<constraints>
<grid row="6" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<model/>
</properties>
</component>
<component id="4225c" class="javax.swing.JTextField" binding="portTextField">
<constraints>
<grid row="5" column="2" row-span="1" col-span="1" vsize-policy="0" hsize-policy="2" anchor="8" fill="1" indent="0" use-parent-layout="false">
<preferred-size width="150" height="-1"/>
</grid>
</constraints>
<properties/>
</component>
<component id="43a89" class="javax.swing.JLabel">
<constraints>
<grid row="6" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Select game:"/>
</properties>
</component>
<component id="3abc3" class="javax.swing.JLabel">
<constraints>
<grid row="5" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Port:"/>
</properties>
</component>
<component id="514db" class="javax.swing.JLabel">
<constraints>
<grid row="4" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="IP:"/>
</properties>
</component>
<component id="5723c" class="javax.swing.JLabel">
<constraints>
<grid row="3" column="1" row-span="1" col-span="1" vsize-policy="0" hsize-policy="0" anchor="8" fill="0" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Name 2:"/>
</properties>
</component>
<hspacer id="2e34c">
<constraints>
<grid row="3" column="0" row-span="1" col-span="1" vsize-policy="1" hsize-policy="6" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
</hspacer>
<vspacer id="93fc6">
<constraints>
<grid row="0" column="2" row-span="1" col-span="1" vsize-policy="6" hsize-policy="1" anchor="0" fill="2" indent="0" use-parent-layout="false"/>
</constraints>
</vspacer>
</children>
</grid>
</form>

View File

@@ -0,0 +1,124 @@
package org.toop.frontend.UI;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.eventbus.Events;
import org.toop.eventbus.GlobalEventBus;
import javax.swing.*;
import java.awt.event.ActionEvent;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class RemoteGameSelector {
private static final Logger logger = LogManager.getLogger(RemoteGameSelector.class);
private JPanel mainMenu;
private JTextField nameTextField;
private JTextField name2TextField;
private JTextField ipTextField;
private JTextField portTextField;
private JButton connectButton;
private JComboBox gameSelectorBox;
private JPanel cards;
private JPanel gameSelector;
private JFrame frame;
private JLabel fillAllFields;
public RemoteGameSelector() {
gameSelectorBox.addItem("Tic Tac Toe");
gameSelectorBox.addItem("Reversi");
//todo get supported games from server and add to gameSelectorBox
frame = new JFrame("Game Selector");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(1920, 1080);
frame.setResizable(true);
init();
frame.add(mainMenu);
frame.setVisible(true);
//GlobalEventBus.subscribeAndRegister() Todo add game panel to frame when connection succeeds
}
private void init() {
connectButton.addActionListener((ActionEvent e) -> {
if( !nameTextField.getText().isEmpty() &&
!name2TextField.getText().isEmpty() &&
!ipTextField.getText().isEmpty() &&
!portTextField.getText().isEmpty()) {
CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
GlobalEventBus.post(new Events.ServerEvents.StartServerRequest(
portTextField.getText(),
Objects.requireNonNull(gameSelectorBox.getSelectedItem()).toString().toLowerCase().replace(" ", ""),
serverIdFuture
));
String serverId;
try {
serverId = serverIdFuture.get();
} catch (InterruptedException | ExecutionException ex) {
throw new RuntimeException(ex);
} // TODO: Better error handling to not crash the system.
CompletableFuture<String> connectionIdFuture = new CompletableFuture<>();
GlobalEventBus.post(new Events.ServerEvents.StartConnectionRequest(
ipTextField.getText(),
portTextField.getText(),
connectionIdFuture
));
String connectionId;
try {
connectionId = connectionIdFuture.get();
} catch (InterruptedException | ExecutionException ex) {
throw new RuntimeException(ex);
} // TODO: Better error handling to not crash the system.
GlobalEventBus.subscribeAndRegister(Events.ServerEvents.ReceivedMessage.class,
event -> {
if (event.message().equalsIgnoreCase("ok")) {
logger.info("received ok from server.");
} else if (event.message().toLowerCase().startsWith("gameid")) {
String gameId = event.message().toLowerCase().replace("gameid ", "");
GlobalEventBus.post(new Events.ServerEvents.SendCommand("start_game " + gameId));
}
else {
logger.info("{}", event.message());
}
}
);
GlobalEventBus.post(new Events.ServerEvents.SendCommand(connectionId, "create_game", nameTextField.getText(), name2TextField.getText()));
// CompletableFuture<String> ticTacToeGame = new CompletableFuture<>();
// GlobalEventBus.post(new Events.ServerEvents.CreateTicTacToeGameRequest( // TODO: Make this happen through commands send through the connection, instead of an event.
// serverId,
// nameTextField.getText(),
// name2TextField.getText(),
// ticTacToeGame
// ));
// String ticTacToeGameId;
// try {
// ticTacToeGameId = ticTacToeGame.get();
// } catch (InterruptedException | ExecutionException ex) {
// throw new RuntimeException(ex);
// } // TODO: Better error handling to not crash the system.
frame.remove(mainMenu);
// UIGameBoard ttt = new UIGameBoard("tic tac toe", "test", "test",this); // TODO: Fix later
// frame.add(ttt.getTTTPanel()); // TODO: Fix later
frame.revalidate();
frame.repaint();
} else {
fillAllFields.setVisible(true);
}
});
}
public void returnToMainMenu() {
frame.removeAll();
frame.add(mainMenu);
frame.revalidate();
frame.repaint();
}
}

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.toop.frontend.UI.Services">
<grid id="27dc6" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="0" left="0" bottom="0" right="0"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties/>
<border type="none"/>
<children/>
</grid>
</form>

View File

@@ -0,0 +1,6 @@
package org.toop.frontend.UI;
import javax.swing.*;
public class Services {
}

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<form xmlns="http://www.intellij.com/uidesigner/form/" version="1" bind-to-class="org.toop.frontend.UI.UIGameBoard">
<grid id="27dc6" binding="tttPanel" layout-manager="GridLayoutManager" row-count="1" column-count="1" same-size-horizontally="false" same-size-vertically="false" hgap="-1" vgap="-1">
<margin top="10" left="10" bottom="10" right="10"/>
<constraints>
<xy x="20" y="20" width="500" height="400"/>
</constraints>
<properties>
<preferredSize width="800" height="600"/>
</properties>
<border type="none"/>
<children>
<component id="e1a78" class="javax.swing.JButton" binding="backToMainMenuButton" default-binding="true">
<constraints>
<grid row="0" column="0" row-span="1" col-span="1" vsize-policy="0" hsize-policy="1" anchor="0" fill="1" indent="0" use-parent-layout="false"/>
</constraints>
<properties>
<text value="Back to main menu"/>
</properties>
</component>
</children>
</grid>
</form>

View File

@@ -0,0 +1,68 @@
package org.toop.frontend.UI;
import org.toop.eventbus.Events;
import org.toop.eventbus.GlobalEventBus;
import org.toop.frontend.games.LocalTicTacToe;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.util.Objects;
public class UIGameBoard {
private static final int TICTACTOE_SIZE = 3;
private JPanel tttPanel; // Root panel for this game
private JButton backToMainMenuButton;
private JButton[] cells;
private String currentPlayer = "X";
private LocalGameSelector parentSelector;
private LocalTicTacToe localTicTacToe;
public UIGameBoard(LocalTicTacToe lttt, LocalGameSelector parent) {
this.parentSelector = parent;
this.localTicTacToe = lttt;
// Root panel
tttPanel = new JPanel(new BorderLayout());
// Back button
backToMainMenuButton = new JButton("Back to Main Menu");
tttPanel.add(backToMainMenuButton, BorderLayout.SOUTH);
backToMainMenuButton.addActionListener(e ->
// TODO reset game and connections
parent.showMainMenu()
);
// Game grid
JPanel gameGrid = createGridPanel(TICTACTOE_SIZE, TICTACTOE_SIZE);
tttPanel.add(gameGrid, BorderLayout.CENTER);
}
private JPanel createGridPanel(int sizeX, int sizeY) {
JPanel panel = new JPanel(new GridLayout(sizeX, sizeY));
cells = new JButton[sizeX * sizeY];
for (int i = 0; i < sizeX * sizeY; i++) {
cells[i] = new JButton(" ");
cells[i].setFont(new Font("Arial", Font.BOLD, 100 / sizeX));
panel.add(cells[i]);
final int index = i;
cells[i].addActionListener((ActionEvent e) -> {
cells[index].setText(currentPlayer);
if (Objects.equals(currentPlayer, "X")) { currentPlayer = "O"; }
else { currentPlayer = "X"; }
this.localTicTacToe.move(index);
System.out.println("Cell clicked: " + index);
});
}
return panel;
}
public JPanel getTTTPanel() {
return tttPanel;
}
}

View File

@@ -0,0 +1,170 @@
package org.toop.frontend.games;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.toop.eventbus.Events;
import org.toop.eventbus.GlobalEventBus;
import java.util.concurrent.*;
public class LocalTicTacToe { // TODO: Implement runnable
private static final Logger logger = LogManager.getLogger(LocalTicTacToe.class);
private final ExecutorService executor = Executors.newSingleThreadExecutor();
private final BlockingQueue<String> receivedQueue = new LinkedBlockingQueue<>();
Object receivedMessageListener;
volatile String gameId;
volatile String connectionId;
volatile String serverId;
/**
* Is either 1 or 2.
*/
private int playersTurn = 1;
// LocalTicTacToe(String gameId, String connectionId, String serverId) {
// this.gameId = gameId;
// this.connectionId = connectionId;
// this.serverId = serverId;
// this.receivedMessageListener = GlobalEventBus.subscribe(Events.ServerEvents.ReceivedMessage.class, this::receiveMessageAction);
// GlobalEventBus.register(this.receivedMessageListener);
//
//
// this.executor.submit(this::gameThread);
// } TODO: If remote server
public LocalTicTacToe(boolean isLocalServer, String ip, String port) {
this.receivedMessageListener = GlobalEventBus.subscribe(Events.ServerEvents.ReceivedMessage.class, this::receiveMessageAction);
GlobalEventBus.register(this.receivedMessageListener);
// TODO: Is blocking
if (isLocalServer) { this.serverId = this.createServer(port); }
else { this.serverId = null; } // TODO: What if null?
this.connectionId = this.createConnection(ip, port);
this.createGame(ip, port);
this.executor.submit(this::gameThread);
}
private String createServer(String port) {
CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
GlobalEventBus.post(new Events.ServerEvents.StartServerRequest(port, "tictactoe", serverIdFuture));
try {
return serverIdFuture.get();
} catch (Exception e) {
logger.error("Error getting server ID", e);
}
return null;
}
private String createConnection(String ip, String port) {
CompletableFuture<String> connectionIdFuture = new CompletableFuture<>();
GlobalEventBus.post(new Events.ServerEvents.StartConnectionRequest(ip, port, connectionIdFuture)); // TODO: what if server couldn't be started with port.
try {
return connectionIdFuture.get();
} catch (InterruptedException | ExecutionException e) {
logger.error("Error getting connection ID", e);
}
return null;
}
private void createGame(String nameA, String nameB) {
nameA = nameA.trim().replace(" ", "-");
nameB = nameB.trim().replace(" ", "-");
this.sendCommand("create_game", nameA, nameB);
}
private void startGame() {
if (this.gameId == null) { return; }
this.sendCommand("start_game", this.gameId);
}
void gameThread() {
logger.info("Starting local game thread, connection: {}, server: {}", this.connectionId, this.serverId);
CountDownLatch latch = new CountDownLatch(1); // TODO: This is bad, fix later
new Thread(() -> {
while(true) {
String msg = this.receivedQueue.poll();
if (msg == null) {continue;}
if (msg.toLowerCase().startsWith("game created successfully")) {
String[] parts = msg.split("\\|");
String gameIdPart = parts[1];
this.gameId = gameIdPart.split(" ")[1];
latch.countDown();
break;
}
}
}).start();
try {
latch.await(); // TODO: Bad, fix later
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
startGame(); // TODO: Actually need to wait, but is fine for now.
boolean running = true;
while (running) {
try {
String rec = this.receivedQueue.take();
if (rec.equalsIgnoreCase("ok")) {continue;}
else if (rec.equalsIgnoreCase("svr game yourturn")) {
if (this.playersTurn == 1) {
this.playersTurn += 1;
} else {
this.playersTurn -= 1;
}
logger.info("Player turn: {}", this.playersTurn);
}
else if (rec.equalsIgnoreCase("svr game win")) {
endListeners();
}
} catch (InterruptedException e) {
throw new RuntimeException(e); // TODO: Error handling
}
}
this.endListeners();
}
public void endGame() {
sendCommand("gameid", "end_game"); // TODO: Command is a bit wrong.
}
public void move(int index) {
sendCommand("gameid", this.gameId, "player", "test", "move", String.valueOf(index));
}
private void endTheGame() {
this.sendCommand("end_game", this.gameId);
this.endListeners();
}
void receiveMessageAction(Events.ServerEvents.ReceivedMessage receivedMessage) {
if (!receivedMessage.ConnectionUuid().equals(this.connectionId)) {
return;
}
try {
logger.info("Received message from " + this.connectionId + ": " + receivedMessage.message());
this.receivedQueue.put(receivedMessage.message());
} catch (InterruptedException e) {
logger.error("Error waiting for received Message", e);
}
}
void sendCommand(String... args) {
GlobalEventBus.post(new Events.ServerEvents.SendCommand(this.connectionId, args));
}
void endListeners() {
GlobalEventBus.unregister(this.receivedMessageListener);
}
}

View File

@@ -0,0 +1,50 @@
package org.toop.frontend.graphics;
import org.toop.frontend.platform.graphics.opengl.OpenglRenderer;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public abstract class Renderer {
public enum API {
NONE,
OPENGL,
};
protected static final Logger logger = LogManager.getLogger(Renderer.class);
private static API api = API.NONE;
private static Renderer instance = null;
public static Renderer setup(API api) {
if (instance != null) {
logger.warn("Renderer is already setup.");
return instance;
}
switch (api) {
case OPENGL:
instance = new OpenglRenderer();
break;
default:
logger.fatal("No valid renderer api chosen");
return null;
}
Renderer.api = api;
return instance;
}
public static API getApi() {
return api;
}
public void cleanup() {
instance = null;
logger.info("Renderer cleanup.");
}
public abstract void clear();
public abstract void render();
}

View File

@@ -0,0 +1,26 @@
package org.toop.frontend.graphics;
import org.toop.frontend.platform.graphics.opengl.OpenglShader;
public abstract class Shader {
public static Shader create(String vertexPath, String fragmentPath) {
Shader shader = null;
switch (Renderer.getApi()) {
case OPENGL:
shader = new OpenglShader(vertexPath, fragmentPath);
break;
case NONE:
default:
break;
}
return shader;
}
public abstract void cleanup();
public abstract void start();
public abstract void stop();
}

View File

@@ -0,0 +1,26 @@
package org.toop.frontend.graphics.node;
import org.toop.core.*;
import org.toop.frontend.math.Color;
public class Button extends Node {
ICallable<Boolean> onHover;
ICallable<Boolean> onClick;
public Button(int x, int y, int width, int height, Color color, ICallable<Boolean> onHover, ICallable<Boolean> onClick) {
super(x, y, width, height, color);
this.onHover = onHover;
this.onClick = onClick;
}
@Override
public void hover() {
onHover.call();
}
@Override
public void click() {
onClick.call();
}
}

View File

@@ -0,0 +1,21 @@
package org.toop.frontend.graphics.node;
import org.toop.frontend.math.Bounds;
import org.toop.frontend.math.Color;
public abstract class Node {
protected Bounds bounds;
protected Color color;
public Node(int x, int y, int width, int height, Color color) {
bounds = new Bounds(x, y, width, height);
this.color = color;
}
public boolean check(int x, int y) {
return bounds.check(x, y);
}
public void hover() {}
public void click() {}
}

View File

@@ -0,0 +1,66 @@
package org.toop.frontend.graphics.node;
import org.toop.eventbus.*;
import org.toop.frontend.graphics.Shader;
import java.util.*;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.LogManager;
public class NodeManager {
private static final Logger logger = LogManager.getLogger(NodeManager.class);
private static NodeManager instance = null;
public static NodeManager setup() {
if (instance != null) {
logger.warn("NodeManager is already setup.");
return instance;
}
instance = new NodeManager();
return instance;
}
private Shader shader;
private ArrayList<Node> nodes;
private Node active;
private NodeManager() {
shader = Shader.create(
"src/main/resources/shaders/gui_vertex.glsl",
"src/main/resources/shaders/gui_fragment.glsl");
nodes = new ArrayList<Node>();
GlobalEventBus.subscribeAndRegister(Events.WindowEvents.OnMouseMove.class, event -> {
for (int i = 0; i < nodes.size(); i++) {
Node node = nodes.get(i);
if (node.check(event.x(), event.y())) {
active = node;
node.hover();
break;
}
}
});
GlobalEventBus.subscribeAndRegister(Events.WindowEvents.OnMouseClick.class, event -> {
if (active != null) {
active.click();
}
});
}
public void cleanup() {
}
public void add(Node node) {
nodes.add(node);
}
public void render() {
}
}

View File

@@ -0,0 +1,49 @@
package org.toop.frontend.graphics.node;
import org.toop.frontend.math.Bounds;
import java.util.*;
public class Widget {
Bounds bounds;
private ArrayList<Node> nodes;
public Widget(Bounds bounds) {
this.bounds = bounds;
nodes = new ArrayList<Node>();
}
public boolean check(int x, int y) {
return bounds.check(x, y);
}
public void add(Node node) {
nodes.add(node);
}
public boolean hover(int x, int y) {
for (int i = 0; i < nodes.size(); i++) {
Node node = nodes.get(i);
if (node.check(x, y)) {
node.hover();
return true;
}
}
return false;
}
public boolean click(int x, int y) {
for (int i = 0; i < nodes.size(); i++) {
Node node = nodes.get(i);
if (node.check(x, y)) {
node.click();
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,30 @@
package org.toop.frontend.math;
public class Bounds {
private int x;
private int y;
private int width;
private int height;
public Bounds(int x, int y, int width, int height) {
set(x, y, width, height);
}
public void set(int x, int y, int width, int height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
}
public int getX() { return x; }
public int getY() { return y; }
public int getWidth() { return width; }
public int getHeight() { return height; }
public boolean check(int x, int y) {
return
x >= this.x && x <= this.x + this.width &&
y >= this.y && y <= this.y + this.height;
}
}

View File

@@ -0,0 +1,17 @@
package org.toop.frontend.math;
public class Color {
private float r;
private float g;
private float b;
public Color(float r, float g, float b) {
this.r = r;
this.g = g;
this.b = b;
}
public float r() { return r; }
public float g() { return g; }
public float b() { return b; }
}

View File

@@ -0,0 +1,95 @@
package org.toop.frontend.platform.core.glfw;
import org.toop.core.*;
import org.toop.eventbus.*;
import org.lwjgl.glfw.*;
import org.lwjgl.system.*;
public class GlfwWindow extends Window {
private long window;
public GlfwWindow(String title, Size size) {
if (!GLFW.glfwInit()) {
logger.fatal("Failed to initialize glfw");
return;
}
GLFW.glfwDefaultWindowHints();
GLFW.glfwWindowHint(GLFW.GLFW_VISIBLE, GLFW.GLFW_FALSE);
GLFW.glfwWindowHint(GLFW.GLFW_RESIZABLE, GLFW.GLFW_TRUE);
GLFWVidMode videoMode = GLFW.glfwGetVideoMode(GLFW.glfwGetPrimaryMonitor());
int width = size.width();
int height = size.height();
if (width <= 0 || height <= 0 || width > videoMode.width() || height > videoMode.height()) {
width = videoMode.width();
height = videoMode.height();
GLFW.glfwWindowHint(GLFW.GLFW_MAXIMIZED, GLFW.GLFW_TRUE);
}
long window = GLFW.glfwCreateWindow(width, height, title, MemoryUtil.NULL, MemoryUtil.NULL);
if (window == MemoryUtil.NULL) {
GLFW.glfwTerminate();
logger.fatal("Failed to create glfw window");
return;
}
int[] widthBuffer = new int[1];
int[] heightBuffer = new int[1];
GLFW.glfwGetWindowSize(window, widthBuffer, heightBuffer);
GLFW.glfwMakeContextCurrent(window);
GLFW.glfwSwapInterval(1);
GLFW.glfwSetWindowCloseCallback(window, (lwindow) -> {
GlobalEventBus.post(new Events.WindowEvents.OnQuitRequested());
});
GLFW.glfwSetFramebufferSizeCallback(window, (lwindow, lwidth, lheight) -> {
GlobalEventBus.post(new Events.WindowEvents.OnResize(new Size(lwidth, lheight)));
});
GLFW.glfwSetCursorPosCallback(window, (lwindow, lx, ly) -> {
GlobalEventBus.post(new Events.WindowEvents.OnMouseMove((int)lx, (int)ly));
});
GLFW.glfwSetMouseButtonCallback(window, (lwindow, lbutton, laction, lmods) -> {
switch (laction) {
case GLFW.GLFW_PRESS:
GlobalEventBus.post(new Events.WindowEvents.OnMouseClick(lbutton));
break;
case GLFW.GLFW_RELEASE:
GlobalEventBus.post(new Events.WindowEvents.OnMouseRelease(lbutton));
break;
default: break;
}
});
this.window = window;
GLFW.glfwShowWindow(window);
logger.info("Glfw window setup. Title: {}. Width: {}. Height: {}.", title, size.width(), size.height());
}
@Override
public void cleanup() {
GLFW.glfwDestroyWindow(window);
GLFW.glfwTerminate();
super.cleanup();
}
@Override
public void update() {
GLFW.glfwSwapBuffers(window);
GLFW.glfwPollEvents();
}
}

View File

@@ -0,0 +1,76 @@
package org.toop.frontend.platform.graphics.opengl;
import org.toop.eventbus.*;
import org.toop.frontend.graphics.Renderer;
import org.toop.frontend.graphics.Shader;
import org.lwjgl.opengl.*;
import org.lwjgl.system.*;
public class OpenglRenderer extends Renderer {
private Shader shader;
private int vao;
public OpenglRenderer() {
GL.createCapabilities();
GL45.glClearColor(0.65f, 0.9f, 0.65f, 1f);
GlobalEventBus.subscribeAndRegister(Events.WindowEvents.OnResize.class, event -> {
GL45.glViewport(0, 0, event.size().width(), event.size().height());
});
logger.info("Opengl renderer setup.");
// Form here on, everything is temporary
float vertices[] = {
-0.5f, 0.5f, 1.0f, 0.0f, 0.0f,
-0.5f, -0.5f, 0.0f, 1.0f, 0.0f,
0.5f, -0.5f, 0.0f, 0.0f, 1.0f,
0.5f, 0.5f, 1.0f, 1.0f, 0.0f,
};
int indicies[] = {
0, 1, 2,
2, 3, 0,
};
vao = GL45.glCreateVertexArrays();
GL45.glBindVertexArray(vao);
int vbo = GL45.glCreateBuffers();
GL45.glBindBuffer(GL45.GL_ARRAY_BUFFER, vbo);
GL45.glBufferData(GL45.GL_ARRAY_BUFFER, vertices, GL45.GL_STATIC_DRAW);
GL45.glVertexAttribPointer(0, 2, GL45.GL_FLOAT, false, 5 * 4, 0);
GL45.glVertexAttribPointer(1, 3, GL45.GL_FLOAT, false, 5 * 4, 2 * 4);
GL45.glEnableVertexAttribArray(0);
GL45.glEnableVertexAttribArray(1);
int ib = GL45.glCreateBuffers();
GL45.glBindBuffer(GL45.GL_ELEMENT_ARRAY_BUFFER, ib);
GL45.glBufferData(GL45.GL_ELEMENT_ARRAY_BUFFER, indicies, GL45.GL_STATIC_DRAW);
shader = Shader.create(
"src/main/resources/shaders/gui_vertex.glsl",
"src/main/resources/shaders/gui_fragment.glsl");
}
@Override
public void cleanup() {
super.cleanup();
}
@Override
public void clear() {
GL45.glClear(GL45.GL_COLOR_BUFFER_BIT);
}
@Override
public void render() {
// temporary
// shader.start();
GL45.glBindVertexArray(vao);
GL45.glDrawElements(GL45.GL_TRIANGLES, 6, GL45.GL_UNSIGNED_INT, MemoryUtil.NULL);
}
}

View File

@@ -0,0 +1,58 @@
package org.toop.frontend.platform.graphics.opengl;
import org.toop.core.*;
import org.toop.frontend.graphics.Shader;
import org.lwjgl.opengl.*;
public class OpenglShader extends Shader {
private int programID;
public OpenglShader(String vertexPath, String fragmentPath) {
FileSystem.File vertexSource = FileSystem.read(vertexPath);
FileSystem.File fragmentSource = FileSystem.read(fragmentPath);
if (vertexSource == null || fragmentPath == null) {
return;
}
programID = GL45.glCreateProgram();
int vertexShader = GL45.glCreateShader(GL45.GL_VERTEX_SHADER);
int fragmentShader = GL45.glCreateShader(GL45.GL_FRAGMENT_SHADER);
GL45.glShaderSource(vertexShader, vertexSource.buffer());
GL45.glShaderSource(fragmentShader, fragmentSource.buffer());
GL45.glCompileShader(vertexShader);
GL45.glCompileShader(fragmentShader);
GL45.glAttachShader(programID, vertexShader);
GL45.glAttachShader(programID, fragmentShader);
GL45.glLinkProgram(programID);
GL45.glValidateProgram(programID);
GL45.glDetachShader(programID, vertexShader);
GL45.glDetachShader(programID, fragmentShader);
GL45.glDeleteShader(vertexShader);
GL45.glDeleteShader(fragmentShader);
}
@Override
public void cleanup() {
stop();
GL45.glDeleteProgram(programID);
}
@Override
public void start() {
GL45.glUseProgram(programID);
}
@Override
public void stop() {
GL45.glUseProgram(0);
}
}