mirror of
https://github.com/2OOP/pism.git
synced 2026-02-04 10:54:51 +00:00
Biggest update in Tic Tac Toe kind
This commit is contained in:
@@ -23,16 +23,46 @@ public class Main {
|
|||||||
|
|
||||||
initSystems();
|
initSystems();
|
||||||
|
|
||||||
ConsoleGui console = new ConsoleGui();
|
CompletableFuture<String> serverIdFuture = new CompletableFuture<>();
|
||||||
GameBase.State state = GameBase.State.INVALID;
|
GlobalEventBus.post(new Events.ServerEvents.StartServerRequest("5001", "tictactoe", serverIdFuture));
|
||||||
|
String serverId = serverIdFuture.get();
|
||||||
|
|
||||||
console.print();
|
CompletableFuture<String> connectionIdFuture = new CompletableFuture<>();
|
||||||
|
GlobalEventBus.post(new Events.ServerEvents.StartConnectionRequest("127.0.0.1", "5001", connectionIdFuture));
|
||||||
|
String connectionId = connectionIdFuture.get();
|
||||||
|
|
||||||
do {
|
CompletableFuture<String> ticTacToeGame = new CompletableFuture<>();
|
||||||
console.print();
|
GlobalEventBus.post(new Events.ServerEvents.CreateTicTacToeGameRequest(serverId, "J", "P", ticTacToeGame));
|
||||||
} while (console.next());
|
String ticTacToeGameId = ticTacToeGame.get();
|
||||||
|
GlobalEventBus.post(new Events.ServerEvents.RunTicTacToeGame(serverId, ticTacToeGameId));
|
||||||
|
|
||||||
console.print();
|
GlobalEventBus.post(new Events.ServerEvents.Command(
|
||||||
|
connectionId,
|
||||||
|
"gameid " + ticTacToeGameId, "player J", "MOVE", "0"
|
||||||
|
));
|
||||||
|
GlobalEventBus.post(new Events.ServerEvents.Command(
|
||||||
|
connectionId,
|
||||||
|
"gameid " + ticTacToeGameId, "player P", "MOVE", "1"
|
||||||
|
));
|
||||||
|
|
||||||
|
// for (int x = 0; x < 20000000; x++) {
|
||||||
|
// CompletableFuture<String> ticTacToeGame = new CompletableFuture<>();
|
||||||
|
// GlobalEventBus.post(new Events.ServerEvents.CreateTicTacToeGameRequest(serverId, "J"+x, "P"+x, ticTacToeGame));
|
||||||
|
// String ticTacToeGameId = ticTacToeGame.get();
|
||||||
|
// GlobalEventBus.post(new Events.ServerEvents.RunTicTacToeGame(serverId, ticTacToeGameId));
|
||||||
|
// GlobalEventBus.post(new Events.ServerEvents.Command(connectionId, "MOVE", "" + x));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// ConsoleGui console = new ConsoleGui();
|
||||||
|
// GameBase.State state = GameBase.State.INVALID;
|
||||||
|
//
|
||||||
|
// console.print();
|
||||||
|
//
|
||||||
|
// do {
|
||||||
|
// console.print();
|
||||||
|
// } while (console.next());
|
||||||
|
//
|
||||||
|
// console.print();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void initSystems() {
|
public static void initSystems() {
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ public class Events implements IEvents {
|
|||||||
/**
|
/**
|
||||||
* Triggers sending a command to a server.
|
* Triggers sending a command to a server.
|
||||||
*/
|
*/
|
||||||
public record Command(String connectionId, String command, String... args) { }
|
public record Command(String connectionId, String... args) { }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Triggers when a command is sent to a server.
|
* Triggers when a command is sent to a server.
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.toop.game.tictactoe;
|
package org.toop.game.tictactoe;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.toop.game.*;
|
import org.toop.game.*;
|
||||||
import org.toop.server.backend.tictactoe.*;
|
import org.toop.server.backend.tictactoe.*;
|
||||||
|
|
||||||
@@ -7,7 +9,11 @@ import java.util.concurrent.BlockingQueue;
|
|||||||
import java.util.concurrent.LinkedBlockingQueue;
|
import java.util.concurrent.LinkedBlockingQueue;
|
||||||
|
|
||||||
public class TicTacToe extends GameBase implements Runnable {
|
public class TicTacToe extends GameBase implements Runnable {
|
||||||
|
|
||||||
|
protected static final Logger logger = LogManager.getLogger(TicTacToe.class);
|
||||||
|
|
||||||
public Thread gameThread;
|
public Thread gameThread;
|
||||||
|
public String gameId;
|
||||||
public BlockingQueue<ParsedCommand> commandQueue = new LinkedBlockingQueue<>();
|
public BlockingQueue<ParsedCommand> commandQueue = new LinkedBlockingQueue<>();
|
||||||
public BlockingQueue<String> sendQueue = new LinkedBlockingQueue<>();
|
public BlockingQueue<String> sendQueue = new LinkedBlockingQueue<>();
|
||||||
|
|
||||||
@@ -18,10 +24,28 @@ public class TicTacToe extends GameBase implements Runnable {
|
|||||||
movesLeft = size * size;
|
movesLeft = size * size;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* Used for the server.
|
||||||
|
*
|
||||||
|
* @param player1
|
||||||
|
* @param player2
|
||||||
|
* @param gameId
|
||||||
|
*/
|
||||||
|
public TicTacToe(String player1, String player2, String gameId) {
|
||||||
|
super(3, new Player(player1, 'X'), new Player(player2, 'O'));
|
||||||
|
movesLeft = size * size;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public void addCommandToQueue(ParsedCommand command) {
|
public void addCommandToQueue(ParsedCommand command) {
|
||||||
commandQueue.add(command);
|
commandQueue.add(command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void addSendToQueue(String send) throws InterruptedException {
|
||||||
|
sendQueue.put(send);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
this.gameThread = new Thread(this::gameThread);
|
this.gameThread = new Thread(this::gameThread);
|
||||||
@@ -33,9 +57,13 @@ public class TicTacToe extends GameBase implements Runnable {
|
|||||||
// String command = getNewestCommand();
|
// String command = getNewestCommand();
|
||||||
// command = this.parseCommand(command).toString();
|
// command = this.parseCommand(command).toString();
|
||||||
// if (command == null) { continue; }
|
// if (command == null) { continue; }
|
||||||
|
try {
|
||||||
if (commandQueue.poll() == null) {
|
ParsedCommand cmd = this.commandQueue.take();
|
||||||
continue;
|
logger.info("Game {}, took command: {}", this.gameId, cmd.originalCommand); // TODO: Fix null gameid
|
||||||
|
this.addSendToQueue("OK");
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
logger.error("Game {} has crashed.", this.gameId);
|
||||||
|
throw new RuntimeException(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Game use the commandQueue to get the commands.
|
// TODO: Game use the commandQueue to get the commands.
|
||||||
|
|||||||
@@ -4,28 +4,50 @@ import org.apache.logging.log4j.LogManager;
|
|||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.toop.server.backend.tictactoe.ParsedCommand;
|
import org.toop.server.backend.tictactoe.ParsedCommand;
|
||||||
|
|
||||||
import java.io.*;
|
import java.io.BufferedReader;
|
||||||
import java.net.*;
|
import java.io.IOException;
|
||||||
|
import java.io.InputStreamReader;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.net.ServerSocket;
|
||||||
|
import java.net.Socket;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.*;
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Lightweight, thread-pool based TCP server base class.
|
||||||
|
*
|
||||||
|
* Responsibilities:
|
||||||
|
* - accept sockets
|
||||||
|
* - hand off socket I/O to connectionExecutor (pooled threads)
|
||||||
|
* - provide thread-safe queues (receivedQueue / sendQueue) to subclasses
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - Subclasses should consume receivedQueue (or call getNewestCommand()) and
|
||||||
|
* use sendQueue to send messages to all clients (or per-client, if implemented).
|
||||||
|
*/
|
||||||
public class TcpServer implements Runnable {
|
public class TcpServer implements Runnable {
|
||||||
|
|
||||||
protected static final Logger logger = LogManager.getLogger(TcpServer.class);
|
protected static final Logger logger = LogManager.getLogger(TcpServer.class);
|
||||||
|
|
||||||
private final ExecutorService executor = Executors.newFixedThreadPool(2);
|
// Executor used for per-connection I/O tasks (reading/writing)
|
||||||
public final BlockingQueue<String> receivedQueue = new LinkedBlockingQueue<>();
|
protected final ExecutorService connectionExecutor = Executors.newCachedThreadPool();
|
||||||
public final BlockingQueue<ParsedCommand> commandQueue = new LinkedBlockingQueue<>();
|
|
||||||
|
// Shared queues for subclasses / consumers
|
||||||
|
public final BlockingQueue<String> receivedQueue = new LinkedBlockingQueue<>(); // unbounded; you may choose bounded
|
||||||
public final BlockingQueue<String> sendQueue = new LinkedBlockingQueue<>();
|
public final BlockingQueue<String> sendQueue = new LinkedBlockingQueue<>();
|
||||||
public final Map<Socket, String> knownPlayers = new HashMap<>();
|
|
||||||
public final Map<String, String> playersGames = new HashMap<>();
|
// (Optional) if you want to associate sockets -> player ids
|
||||||
public final int WAIT_TIME = 500; // MS
|
public final Map<Socket, String> knownPlayers = new ConcurrentHashMap<>();
|
||||||
|
public final Map<String, String> playersGames = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
// tunables
|
||||||
|
public final int WAIT_TIME = 500; // ms used by poll-based methods
|
||||||
public final int RETRY_ATTEMPTS = 3;
|
public final int RETRY_ATTEMPTS = 3;
|
||||||
|
|
||||||
protected int port;
|
protected final int port;
|
||||||
protected ServerSocket serverSocket = null;
|
protected final ServerSocket serverSocket;
|
||||||
private boolean running = true;
|
private volatile boolean running = true;
|
||||||
|
|
||||||
public TcpServer(int port) throws IOException {
|
public TcpServer(int port) throws IOException {
|
||||||
this.port = port;
|
this.port = port;
|
||||||
@@ -33,125 +55,148 @@ public class TcpServer implements Runnable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public boolean isRunning() {
|
public boolean isRunning() {
|
||||||
return this.running;
|
return running;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default run: accept connections and hand off to connectionExecutor.
|
||||||
|
* Subclasses overriding run() should still call startWorkers(Socket) for each accepted socket.
|
||||||
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
|
||||||
logger.info("Server listening on port {}", port);
|
logger.info("Server listening on port {}", port);
|
||||||
|
try {
|
||||||
while (running) {
|
while (running) {
|
||||||
Socket clientSocket = this.serverSocket.accept();
|
Socket clientSocket = serverSocket.accept();
|
||||||
logger.info("Connected to client: {}", clientSocket.getInetAddress());
|
logger.info("Accepted connection from {}", clientSocket.getRemoteSocketAddress());
|
||||||
|
// hand off to pool to manage I/O for this socket
|
||||||
new Thread(() -> this.startWorkers(clientSocket)).start();
|
connectionExecutor.submit(() -> startWorkers(clientSocket));
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
if (running) {
|
||||||
|
logger.error("Accept failed", e);
|
||||||
|
} else {
|
||||||
|
logger.info("Server socket closed, stopping acceptor");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runGame() {}
|
/**
|
||||||
|
* Listen/Write workers for an accepted client socket.
|
||||||
public void endGame() {}
|
* This method submits two tasks to the connectionExecutor:
|
||||||
|
* - inputLoop: reads lines and enqueues them to receivedQueue
|
||||||
public void newGame() {}
|
* - outputLoop: polls sendQueue and writes messages to the client
|
||||||
|
*
|
||||||
protected String sendServerMessage() {
|
* Note: This is a simple model where sendQueue is global; if you need per-client
|
||||||
try { return sendQueue.poll(this.WAIT_TIME, TimeUnit.MILLISECONDS); }
|
* send-queues, adapt this method to use one per socket.
|
||||||
catch (InterruptedException e) {
|
*/
|
||||||
logger.error("Interrupted", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected ParsedCommand getNewestCommand() {
|
|
||||||
try {
|
|
||||||
String rec = receivedQueue.poll(this.WAIT_TIME, TimeUnit.MILLISECONDS);
|
|
||||||
if (rec != null) {
|
|
||||||
return new ParsedCommand(rec);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (InterruptedException e) {
|
|
||||||
logger.error("Interrupted", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
//
|
|
||||||
// protected void sendMessage(String message) throws InterruptedException {
|
|
||||||
// sendQueue.put(message);
|
|
||||||
// }
|
|
||||||
|
|
||||||
protected void startWorkers(Socket clientSocket) {
|
protected void startWorkers(Socket clientSocket) {
|
||||||
running = true;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
|
||||||
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
|
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
|
||||||
|
|
||||||
this.executor.submit(() -> this.inputLoop(in));
|
// Input task: read lines and put them on receivedQueue
|
||||||
this.executor.submit(() -> this.outputLoop(out));
|
Runnable inputTask = () -> {
|
||||||
} catch (Exception e) {
|
logger.info("Starting read loop for {}", clientSocket.getRemoteSocketAddress());
|
||||||
logger.error("Server could not start, {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private void stopWorkers() {
|
|
||||||
this.running = false;
|
|
||||||
this.receivedQueue.clear();
|
|
||||||
this.sendQueue.clear();
|
|
||||||
this.executor.shutdownNow();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void inputLoop(BufferedReader in) {
|
|
||||||
|
|
||||||
logger.info("Starting {} connection read", this.port);
|
|
||||||
try {
|
try {
|
||||||
String message;
|
String line;
|
||||||
while (running && (message = in.readLine()) != null) {
|
while (running && (line = in.readLine()) != null) {
|
||||||
logger.info("Received: '{}'", message);
|
if (line.isEmpty()) continue;
|
||||||
if (!message.isEmpty()) {
|
logger.debug("Received from {}: {}", clientSocket.getRemoteSocketAddress(), line);
|
||||||
String finalMessage = message;
|
|
||||||
new Thread(() -> {
|
boolean offered = false;
|
||||||
for (int i = 0; i < this.RETRY_ATTEMPTS; i++) {
|
for (int i = 0; i < RETRY_ATTEMPTS && !offered; i++) {
|
||||||
if (this.receivedQueue.offer(finalMessage)) break;
|
try {
|
||||||
|
// Use offer to avoid blocking indefinitely; adapt timeout/policy as needed
|
||||||
|
offered = this.receivedQueue.offer(line, 200, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (InterruptedException ie) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}).start();
|
}
|
||||||
|
|
||||||
|
if (!offered) {
|
||||||
|
logger.warn("Backpressure: dropping line from {}: {}", clientSocket.getRemoteSocketAddress(), line);
|
||||||
|
// Policy choice: drop, notify, or close connection. We drop here.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("Error reading from server", e);
|
logger.info("Connection closed by remote: {}", clientSocket.getRemoteSocketAddress());
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
this.serverSocket.close();
|
clientSocket.close();
|
||||||
logger.info("Client disconnected. {}", this.port);
|
} catch (IOException ignored) {}
|
||||||
} catch (IOException e) {
|
logger.info("Stopped read loop for {}", clientSocket.getRemoteSocketAddress());
|
||||||
e.printStackTrace();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private void outputLoop(PrintWriter out) {
|
// Output task: poll global sendQueue and write to this specific client.
|
||||||
logger.info("Starting {} connection write", this.port);
|
// NOTE: With a single global sendQueue, every message is sent to every connected client.
|
||||||
|
// If you want per-client sends, change this to use per-client queue map.
|
||||||
|
Runnable outputTask = () -> {
|
||||||
|
logger.info("Starting write loop for {}", clientSocket.getRemoteSocketAddress());
|
||||||
try {
|
try {
|
||||||
while (this.running) {
|
while (running && !clientSocket.isClosed()) {
|
||||||
String send = this.sendQueue.poll(this.WAIT_TIME, TimeUnit.MILLISECONDS);
|
String msg = sendQueue.poll(WAIT_TIME, TimeUnit.MILLISECONDS);
|
||||||
if (send != null) {
|
if (msg != null) {
|
||||||
out.println(send);
|
out.println(msg);
|
||||||
logger.info("Sent message from server {}: '{}'", this.port, send);
|
out.flush();
|
||||||
|
logger.debug("Sent to {}: {}", clientSocket.getRemoteSocketAddress(), msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
Thread.currentThread().interrupt();
|
Thread.currentThread().interrupt();
|
||||||
|
logger.info("Writer interrupted for {}", clientSocket.getRemoteSocketAddress());
|
||||||
|
} catch (Exception e) {
|
||||||
|
logger.error("Writer error for {}: {}", clientSocket.getRemoteSocketAddress(), e.toString());
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
clientSocket.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
logger.info("Stopped write loop for {}", clientSocket.getRemoteSocketAddress());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Submit tasks - they will run on the shared connectionExecutor
|
||||||
|
connectionExecutor.submit(inputTask);
|
||||||
|
connectionExecutor.submit(outputTask);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
logger.error("Could not start workers for client: {}", e.toString());
|
||||||
|
try {
|
||||||
|
clientSocket.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience: wrapper to obtain the latest command (non-blocking poll).
|
||||||
|
* Subclasses can use this, but for blocking behavior consider using receivedQueue.take()
|
||||||
|
*/
|
||||||
|
protected ParsedCommand getNewestCommand() {
|
||||||
|
try {
|
||||||
|
String rec = receivedQueue.poll(WAIT_TIME, TimeUnit.MILLISECONDS);
|
||||||
|
if (rec != null) return new ParsedCommand(rec);
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.warn("Interrupted while polling receivedQueue", e);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop server and cleanup executors/sockets.
|
||||||
|
*/
|
||||||
public void stop() {
|
public void stop() {
|
||||||
stopWorkers();
|
running = false;
|
||||||
logger.info("sendQueue: {}", this.sendQueue.toString());
|
|
||||||
logger.info("receivedQueue: {}", this.receivedQueue.toString());
|
try {
|
||||||
|
serverSocket.close();
|
||||||
|
} catch (IOException ignored) {}
|
||||||
|
|
||||||
|
connectionExecutor.shutdownNow();
|
||||||
|
|
||||||
|
logger.info("TcpServer stopped. receivedQueue size={}, sendQueue size={}",
|
||||||
|
receivedQueue.size(), sendQueue.size());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,16 @@
|
|||||||
package org.toop.server.backend.tictactoe;
|
package org.toop.server.backend.tictactoe;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.toop.Main;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
public class ParsedCommand {
|
public class ParsedCommand {
|
||||||
|
private static final Logger logger = LogManager.getLogger(ParsedCommand.class);
|
||||||
|
|
||||||
public TicTacToeServerCommand command;
|
public TicTacToeServerCommand command;
|
||||||
public ArrayList<Object> arguments;
|
public ArrayList<Object> arguments;
|
||||||
public boolean isValidCommand;
|
public boolean isValidCommand;
|
||||||
@@ -10,10 +18,15 @@ public class ParsedCommand {
|
|||||||
public TicTacToeServerMessage returnMessage;
|
public TicTacToeServerMessage returnMessage;
|
||||||
public String errorMessage;
|
public String errorMessage;
|
||||||
public String originalCommand;
|
public String originalCommand;
|
||||||
|
public String gameId;
|
||||||
|
public String player;
|
||||||
|
|
||||||
public ParsedCommand(String receivedCommand) {
|
public ParsedCommand(String receivedCommand) {
|
||||||
|
|
||||||
if (receivedCommand.isEmpty()) {
|
if (receivedCommand.isEmpty()) {
|
||||||
|
logger.info("Received empty command");
|
||||||
|
this.gameId = null;
|
||||||
|
this.player = null;
|
||||||
this.command = null;
|
this.command = null;
|
||||||
this.arguments = null;
|
this.arguments = null;
|
||||||
this.isValidCommand = false;
|
this.isValidCommand = false;
|
||||||
@@ -24,6 +37,30 @@ public class ParsedCommand {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Case-insensitive regex to match: game_id {id} player {name}
|
||||||
|
Pattern pattern = Pattern.compile(
|
||||||
|
"(?i)\\bgame[_]?id\\s+(\\S+)\\s+player\\s+(\\S+)", Pattern.CASE_INSENSITIVE);
|
||||||
|
Matcher matcher = pattern.matcher(receivedCommand);
|
||||||
|
|
||||||
|
String tempGameId = null;
|
||||||
|
String tempPlayer = null;
|
||||||
|
String tempPayload = receivedCommand;
|
||||||
|
|
||||||
|
if (matcher.find()) {
|
||||||
|
tempGameId = matcher.group(1); // first capture group → game_id
|
||||||
|
tempPlayer = matcher.group(2); // second capture group → player
|
||||||
|
// Remove the matched part from the original command
|
||||||
|
tempPayload = matcher.replaceFirst("").trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.gameId = tempGameId;
|
||||||
|
this.player = tempPlayer;
|
||||||
|
receivedCommand = tempPayload;
|
||||||
|
|
||||||
|
logger.info("Received gameId: {}", gameId);
|
||||||
|
logger.info("Received player: {}", player);
|
||||||
|
logger.info("Received command: {}", receivedCommand);
|
||||||
|
|
||||||
String[] segments = receivedCommand.split(" ");
|
String[] segments = receivedCommand.split(" ");
|
||||||
if (segments[0].isEmpty()) {
|
if (segments[0].isEmpty()) {
|
||||||
this.command = null;
|
this.command = null;
|
||||||
|
|||||||
@@ -2,106 +2,154 @@ package org.toop.server.backend.tictactoe;
|
|||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.toop.game.tictactoe.*;
|
import org.toop.game.tictactoe.TicTacToe;
|
||||||
import org.toop.server.backend.TcpServer;
|
import org.toop.server.backend.TcpServer;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.Socket;
|
import java.net.Socket;
|
||||||
import java.util.*;
|
import java.util.Map;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.*;
|
||||||
|
|
||||||
public class TicTacToeServer extends TcpServer {
|
public class TicTacToeServer extends TcpServer {
|
||||||
|
|
||||||
protected static final Logger logger = LogManager.getLogger(TicTacToeServer.class);
|
protected static final Logger logger = LogManager.getLogger(TicTacToeServer.class);
|
||||||
/**
|
|
||||||
* Map of gameId -> Game instances
|
private final ExecutorService connectionExecutor = Executors.newCachedThreadPool(); // socket I/O
|
||||||
*/
|
private final ExecutorService dispatcherExecutor;
|
||||||
|
private final ExecutorService forwarderExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
|
||||||
|
private final BlockingQueue<ParsedCommand> incomingCommands;
|
||||||
private final Map<String, TicTacToe> games = new ConcurrentHashMap<>();
|
private final Map<String, TicTacToe> games = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
public TicTacToeServer(int port) throws IOException {
|
public TicTacToeServer(int port) throws IOException {
|
||||||
super(port);
|
super(port);
|
||||||
|
|
||||||
|
int dispatchers = Math.max(2, Runtime.getRuntime().availableProcessors());
|
||||||
|
this.dispatcherExecutor = Executors.newFixedThreadPool(dispatchers + 1); // TODO: Magic number for forwardMessages
|
||||||
|
this.incomingCommands = new LinkedBlockingQueue<>(5_000);
|
||||||
|
|
||||||
|
forwarderExecutor.submit(this::forwardLoop);
|
||||||
|
|
||||||
|
for (int i = 0; i < dispatchers; i++) {
|
||||||
|
dispatcherExecutor.submit(this::dispatchLoop);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
logger.info("Tic tac toe server listening on port {}", this.port);
|
logger.info("TicTacToe server listening on port {}", this.port);
|
||||||
|
|
||||||
while (isRunning()) {
|
while (isRunning()) {
|
||||||
Socket clientSocket = this.serverSocket.accept();
|
Socket clientSocket = this.serverSocket.accept();
|
||||||
logger.info("Connected to client: {}", clientSocket.getInetAddress());
|
logger.info("Connected to client: {}", clientSocket.getInetAddress());
|
||||||
|
|
||||||
new Thread(() -> this.startWorkers(clientSocket)).start();
|
connectionExecutor.submit(() -> this.startWorkers(clientSocket));
|
||||||
new Thread(this::gameManagerThread).start();
|
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
e.printStackTrace();
|
logger.error("I/O error in server run loop", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
/**
|
||||||
protected ParsedCommand getNewestCommand() {
|
* Forwards raw messages from TcpServer.receivedQueue into ParsedCommand objects.
|
||||||
|
*/
|
||||||
|
private void forwardLoop() {
|
||||||
|
logger.info("Forwarder loop started");
|
||||||
try {
|
try {
|
||||||
String rec = receivedQueue.poll(this.WAIT_TIME, TimeUnit.MILLISECONDS);
|
while (isRunning()) {
|
||||||
if (rec != null) {
|
String raw = this.receivedQueue.take(); // blocks
|
||||||
return new ParsedCommand(rec);
|
logger.info("Received command: {}", raw);
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (InterruptedException e) {
|
|
||||||
logger.error("Interrupted", e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void gameManagerThread() {
|
|
||||||
while (true) { // TODO: Very heavy on thread
|
|
||||||
try {
|
try {
|
||||||
synchronized (this) {
|
ParsedCommand pc = new ParsedCommand(raw);
|
||||||
wait(250);
|
this.incomingCommands.put(pc); // blocks if full
|
||||||
} // Fixes current thread is not owner.
|
} catch (Exception e) {
|
||||||
|
logger.warn("Invalid message ignored: {}", raw, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (InterruptedException e) {
|
} catch (InterruptedException e) {
|
||||||
logger.error("Interrupted", e);
|
Thread.currentThread().interrupt();
|
||||||
}
|
logger.info("Forwarder loop interrupted");
|
||||||
ParsedCommand command = getNewestCommand();
|
|
||||||
if (command != null && !command.isServerCommand) {
|
|
||||||
TicTacToe testGame = games.values().iterator().next(); // TODO: Is to get first for testing, must be done a different way later.
|
|
||||||
testGame.addCommandToQueue(command);
|
|
||||||
logger.info("Added command to the game queue: {}", command);
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches parsed commands into the game logic.
|
||||||
|
*/
|
||||||
|
private void dispatchLoop() {
|
||||||
|
logger.info("Dispatcher thread started");
|
||||||
|
try {
|
||||||
|
while (isRunning()) {
|
||||||
|
ParsedCommand command = this.incomingCommands.take(); // blocks
|
||||||
|
if (command.isServerCommand) {
|
||||||
|
handleServerCommand(command);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find game by ID
|
||||||
|
TicTacToe game = this.games.get(command.gameId);
|
||||||
|
if (game != null) {
|
||||||
|
game.addCommandToQueue(command);
|
||||||
|
logger.info("Dispatched command {} to game {}", command.toString(), command.gameId);
|
||||||
|
} else {
|
||||||
|
logger.warn("No active game with ID {} for command {}", command.gameId, command.toString());
|
||||||
|
// TODO: reply back
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
logger.info("Dispatcher interrupted");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleServerCommand(ParsedCommand command) {
|
||||||
|
|
||||||
|
// TODO
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void forwardGameMessages(TicTacToe game) {
|
||||||
|
dispatcherExecutor.submit(() -> {
|
||||||
|
try {
|
||||||
|
while (isRunning()) {
|
||||||
|
String msg = game.sendQueue.take(); // blocks until a message is added to the queue
|
||||||
|
logger.info("Adding: {} to the send queue", msg);
|
||||||
|
this.sendQueue.put(msg); // push to network layer
|
||||||
|
}
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public String newGame(String playerA, String playerB) {
|
public String newGame(String playerA, String playerB) {
|
||||||
logger.info("Creating a new game: {} vs {}", playerA, playerB);
|
logger.info("Creating a new game: {} vs {}", playerA, playerB);
|
||||||
String gameId = UUID.randomUUID().toString();
|
String gameId = UUID.randomUUID().toString();
|
||||||
TicTacToe game = new TicTacToe(playerA, playerB);
|
TicTacToe game = new TicTacToe(playerA, playerB, gameId);
|
||||||
this.games.put(gameId, game);
|
this.games.put(gameId, game);
|
||||||
// this.knownPlayers.put(sockA, playerA); // TODO: For remembering players and validation.
|
forwardGameMessages(game);
|
||||||
// this.knownPlayers.put(sockB, playerB);
|
logger.info("Created new game: {}. {} vs {}", gameId, playerA, playerB);
|
||||||
// this.playersGames.put(playerA, gameId);
|
|
||||||
// this.playersGames.put(playerB, gameId);
|
|
||||||
logger.info("Created a new game: {}. {} vs {}", gameId, playerA, playerB);
|
|
||||||
return gameId;
|
return gameId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void runGame(String gameId) {
|
public void runGame(String gameId) {
|
||||||
TicTacToe game = this.games.get(gameId);
|
TicTacToe game = this.games.get(gameId);
|
||||||
|
if (game != null) {
|
||||||
game.run();
|
game.run();
|
||||||
logger.info("Running game: {}, players: {}", gameId, game.getPlayers());
|
logger.info("Running game: {}, players: {}", gameId, game.getPlayers());
|
||||||
|
} else {
|
||||||
|
logger.warn("Tried to run unknown game {}", gameId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void endGame(String gameId) {
|
public void endGame(String gameId) {
|
||||||
TicTacToe game = this.games.get(gameId);
|
TicTacToe game = this.games.remove(gameId);
|
||||||
this.games.remove(gameId);
|
if (game != null) {
|
||||||
// this.knownPlayers.put(sockA, playerA); // TODO: Remove players when game is done.
|
|
||||||
// this.knownPlayers.put(sockB, playerB);
|
|
||||||
// this.playersGames.put(playerA, gameId);
|
|
||||||
// this.playersGames.put(playerB, gameId);
|
|
||||||
logger.info("Ended game: {}", gameId);
|
logger.info("Ended game: {}", gameId);
|
||||||
// TODO: Multithreading, close game in a graceful matter, etc.
|
// TODO: gracefully stop game thread
|
||||||
|
} else {
|
||||||
|
logger.warn("Tried to end unknown game {}", gameId);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -55,12 +55,12 @@ public class ConnectionManager {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleCommand(Events.ServerEvents.Command event) {
|
private void handleCommand(Events.ServerEvents.Command event) { // TODO: Move this to ServerConnection class, keep it internal.
|
||||||
ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
|
ServerConnection serverConnection = this.serverConnections.get(event.connectionId());
|
||||||
if (serverConnection != null) {
|
if (serverConnection != null) {
|
||||||
serverConnection.sendCommandByString(event.command(), event.args());
|
serverConnection.sendCommandByString(event.args());
|
||||||
} else {
|
} else {
|
||||||
logger.warn("Server {} not found for command '{}'", event.connectionId(), event.command());
|
logger.warn("Server {} not found for command '{}'", event.connectionId(), event.args());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ public final class ServerConnection implements Runnable {
|
|||||||
volatile boolean running = false;
|
volatile boolean running = false;
|
||||||
|
|
||||||
public ServerConnection(String uuid, String ip, String port) {
|
public ServerConnection(String uuid, String ip, String port) {
|
||||||
|
this.uuid = uuid;
|
||||||
this.ip = ip;
|
this.ip = ip;
|
||||||
this.port = port;
|
this.port = port;
|
||||||
this.initEvents();
|
this.initEvents();
|
||||||
@@ -45,30 +46,18 @@ public final class ServerConnection implements Runnable {
|
|||||||
* @param command The command to send to the server.
|
* @param command The command to send to the server.
|
||||||
* @param args The arguments for the command.
|
* @param args The arguments for the command.
|
||||||
*/
|
*/
|
||||||
public void sendCommandByString(String command, String... args) {
|
public void sendCommandByString(String... args) {
|
||||||
if (!TicTacToeServerCommand.isValid(command)) {
|
// if (!TicTacToeServerCommand.isValid(command)) {
|
||||||
logger.error("Invalid command: {}", command);
|
// logger.error("Invalid command: {}", command);
|
||||||
return;
|
// return;
|
||||||
}
|
// } // TODO: DO I CARE?
|
||||||
|
|
||||||
System.out.println();
|
|
||||||
|
|
||||||
if (!this.running) {
|
if (!this.running) {
|
||||||
logger.warn("Server has been stopped");
|
logger.warn("Server has been stopped");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (int i = 0; i < args.length; i++) {
|
String command = String.join(" ", args);
|
||||||
args[i] = args[i].trim();
|
|
||||||
if (args[i].isEmpty()) {
|
|
||||||
throw new IllegalArgumentException("Empty argument"); // TODO: Error handling, just crashes atm.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
String[] fullCommand = new String[args.length + 1];
|
|
||||||
fullCommand[0] = command;
|
|
||||||
System.arraycopy(args, 0, fullCommand, 1, args.length);
|
|
||||||
command = String.join(" ", fullCommand);
|
|
||||||
|
|
||||||
this.commandQueue.add(command);
|
this.commandQueue.add(command);
|
||||||
logger.info("Command '{}' added to the queue", command);
|
logger.info("Command '{}' added to the queue", command);
|
||||||
|
|||||||
Reference in New Issue
Block a user