diff --git a/ai-client/build.sh b/ai-client/build.sh
new file mode 100755
index 0000000..a3fa2bd
--- /dev/null
+++ b/ai-client/build.sh
@@ -0,0 +1,14 @@
+#!/bin/bash
+echo "Building AI Client..."
+mvn clean package
+
+if [ $? -eq 0 ]; then
+ echo "--------------------------------------------------"
+ echo "Build Successful!"
+ echo "Run AI Client:"
+ echo "java -jar target/ai-client-1.0-SNAPSHOT.jar [host] [port]"
+ echo "--------------------------------------------------"
+else
+ echo "Build Failed!"
+ exit 1
+fi
diff --git a/ai-client/pom.xml b/ai-client/pom.xml
new file mode 100644
index 0000000..57bbf15
--- /dev/null
+++ b/ai-client/pom.xml
@@ -0,0 +1,35 @@
+
+ 4.0.0
+ com.lona.tictactoe
+ ai-client
+ 1.0-SNAPSHOT
+
+ 17
+ 17
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.0
+
+
+ package
+
+ shade
+
+
+
+
+ com.lona.tictactoe.client.Main
+
+
+
+
+
+
+
+
+
diff --git a/ai-client/src/main/java/com/lona/tictactoe/client/Main.java b/ai-client/src/main/java/com/lona/tictactoe/client/Main.java
new file mode 100644
index 0000000..b94e4d3
--- /dev/null
+++ b/ai-client/src/main/java/com/lona/tictactoe/client/Main.java
@@ -0,0 +1,406 @@
+package com.lona.tictactoe.client;
+
+import javax.swing.*;
+import java.awt.*;
+import java.io.*;
+import java.net.Socket;
+import java.net.http.HttpClient;
+import java.net.http.HttpRequest;
+import java.net.http.HttpResponse;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class Main extends JFrame {
+ private Socket socket;
+ private PrintWriter out;
+ private BufferedReader in;
+
+ private CardLayout cardLayout = new CardLayout();
+ private JPanel mainPanel = new JPanel(cardLayout);
+
+ // Menu Components
+ private JTextField joinCodeField = new JTextField(10);
+
+ // Game Components
+ private JButton[] buttons = new JButton[9];
+ private JLabel statusLabel = new JLabel("Connecting...");
+ private JLabel codeLabel = new JLabel("Code: -");
+ private boolean myTurn = false;
+ private char mySymbol = ' '; // assigned by server implied turn
+
+ public static void main(String[] args) {
+ String host = "38.242.130.81";
+ int port = 1870;
+
+ if (args.length > 0) {
+ host = args[0];
+ }
+ if (args.length > 1) {
+ try {
+ port = Integer.parseInt(args[1]);
+ } catch (NumberFormatException e) {
+ System.err.println("Invalid port number provided, using default 1870");
+ }
+ }
+
+ final String serverHost = host;
+ final int serverPort = port;
+
+ System.out.println("Starting TicTacToe Client...");
+ System.out.println("Target Server: " + serverHost + ":" + serverPort);
+
+ SwingUtilities.invokeLater(() -> {
+ try {
+ Main client = new Main(serverHost, serverPort);
+ client.setVisible(true);
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.err.println("Failed to start GUI: " + e.getMessage());
+ }
+ });
+ }
+
+ // Instance variables
+ private final String serverHost;
+ private final int serverPort;
+
+ public Main(String host, int port) {
+ super("TicTacToe Client");
+ this.serverHost = host;
+ this.serverPort = port;
+
+ setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
+ setSize(400, 500);
+ setLocationRelativeTo(null);
+
+ try {
+ initUI();
+ setContentPane(mainPanel);
+ connectToServer();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ private void initUI() {
+ // --- MENU PANEL ---
+ JPanel menuPanel = new JPanel(new GridBagLayout());
+ JButton createBtn = new JButton("Create Game");
+ JButton joinBtn = new JButton("Join Game");
+
+ createBtn.addActionListener(e -> send("CREATE"));
+ joinBtn.addActionListener(e -> {
+ String code = joinCodeField.getText().trim();
+ if (!code.isEmpty())
+ send("JOIN " + code);
+ });
+
+ GridBagConstraints gbc = new GridBagConstraints();
+ gbc.insets = new Insets(5, 5, 5, 5);
+ gbc.gridx = 0;
+ gbc.gridy = 0;
+ menuPanel.add(createBtn, gbc);
+
+ gbc.gridy = 1;
+ menuPanel.add(new JLabel("Enter Code:"), gbc);
+ gbc.gridy = 2;
+ menuPanel.add(joinCodeField, gbc);
+ gbc.gridy = 3;
+ menuPanel.add(joinBtn, gbc);
+
+ // --- GAME PANEL ---
+ JPanel gamePanel = new JPanel(new BorderLayout());
+
+ JPanel topPanel = new JPanel(new GridLayout(2, 1));
+ topPanel.add(statusLabel);
+ topPanel.add(codeLabel);
+ gamePanel.add(topPanel, BorderLayout.NORTH);
+
+ JPanel boardPanel = new JPanel(new GridLayout(3, 3));
+ for (int i = 0; i < 9; i++) {
+ int finalI = i;
+ buttons[i] = new JButton("");
+ buttons[i].setFont(new Font("Arial", Font.BOLD, 40));
+ buttons[i].setFocusPainted(false);
+ buttons[i].addActionListener(e -> {
+ int r = finalI / 3;
+ int c = finalI % 3;
+ send("MOVE " + r + " " + c);
+ });
+ boardPanel.add(buttons[i]);
+ }
+ gamePanel.add(boardPanel, BorderLayout.CENTER);
+
+ // --- BUTTONS PANEL ---
+ JPanel buttonPanel = new JPanel(new GridLayout(1, 2, 10, 0));
+
+ JButton leaveBtn = new JButton("Leave Lobby");
+ leaveBtn.setBackground(Color.GRAY);
+ leaveBtn.setForeground(Color.WHITE);
+ leaveBtn.addActionListener(e -> {
+ send("LEAVE");
+ resetGame();
+ });
+
+ JButton surrenderBtn = new JButton("Surrender");
+ surrenderBtn.setBackground(new Color(220, 53, 69)); // Bootstrap Danger Red
+ surrenderBtn.setForeground(Color.WHITE);
+ surrenderBtn.addActionListener(e -> send("SURRENDER"));
+
+ buttonPanel.add(leaveBtn);
+ buttonPanel.add(surrenderBtn);
+
+ gamePanel.add(buttonPanel, BorderLayout.SOUTH);
+
+ mainPanel.add(menuPanel, "MENU");
+ mainPanel.add(gamePanel, "GAME");
+ }
+
+ // AI Configuration
+ private static final String API_KEY = "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";
+ private static final String MODEL = "arcee-ai/trinity-large-preview:free";
+ private char[] boardState = new char[9]; // Keep track of board state
+
+ private void connectToServer() {
+ new Thread(() -> {
+ try {
+ System.out.println("Attempting to connect to " + serverHost + ":" + serverPort);
+ socket = new Socket(serverHost, serverPort);
+ out = new PrintWriter(socket.getOutputStream(), true);
+ in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+
+ System.out.println("Connected successfully!");
+ SwingUtilities.invokeLater(() -> statusLabel.setText("Connected to Server."));
+
+ String line;
+ while ((line = in.readLine()) != null) {
+ System.out.println("Received: " + line); // Log received messages
+ processMessage(line);
+ }
+ } catch (IOException e) {
+ System.err.println("Connection Error: " + e.getMessage());
+ SwingUtilities.invokeLater(() -> {
+ statusLabel.setText("Connection Failed.");
+ JOptionPane.showMessageDialog(this, "Connection Error: " + e.getMessage());
+ // Don't reset to menu if we can't connect, let user see error
+ });
+ }
+ }).start();
+ }
+
+ private void send(String msg) {
+ if (out != null)
+ out.println(msg);
+ }
+
+ private void processMessage(String msg) {
+ SwingUtilities.invokeLater(() -> handleMessage(msg));
+ }
+
+ private void handleMessage(String msg) {
+ String[] parts = msg.split(" ");
+ String cmd = parts[0];
+
+ switch (cmd) {
+ case "GAME_CREATED":
+ cardLayout.show(mainPanel, "GAME");
+ if (parts.length > 1) {
+ codeLabel.setText("Code: " + parts[1]);
+ mySymbol = 'X'; // Creator starts as X usually
+ statusLabel.setText("Game Created. Waiting for opponent...");
+ }
+ break;
+ case "JOIN_SUCCESS":
+ cardLayout.show(mainPanel, "GAME");
+ mySymbol = 'O'; // Joiner is O
+ statusLabel.setText("Joined Game.");
+ codeLabel.setText("Code: " + joinCodeField.getText());
+ break;
+ case "START":
+ statusLabel.setText("Game Started! X goes first.");
+ break;
+ case "RESTART":
+ for (int i = 0; i < 9; i++) {
+ buttons[i].setText("");
+ boardState[i] = ' ';
+ }
+ statusLabel.setText("Game Restarted! X goes first.");
+ break;
+ case "BOARD": // BOARD X O X ...
+ String boardStr = msg.substring(6); // remove "BOARD "
+ for (int i = 0; i < 9 && i < boardStr.length(); i++) {
+ char c = boardStr.charAt(i);
+ boardState[i] = c;
+ buttons[i].setText(String.valueOf(c));
+ }
+ break;
+ case "TURN":
+ if (parts.length > 1) {
+ char turn = parts[1].charAt(0);
+ if (turn == mySymbol) {
+ statusLabel.setText("AI Turn (" + mySymbol + ")... Thinking...");
+ makeAiMove();
+ } else {
+ statusLabel.setText("Opponent's Turn (" + turn + ")");
+ }
+ }
+ break;
+ case "WIN":
+ String winner = msg.substring(4);
+ statusLabel.setText("Winner: " + winner);
+ // Auto RESTART if AI loses? Or wait for user? Let's just create a dialog but
+ // maybe auto-accept if we want seamless play? No, user requested seamless
+ // winning.
+ // Let's just wait for user to restart.
+ JOptionPane.showMessageDialog(this, "Winner: " + winner);
+ break;
+ case "DRAW":
+ statusLabel.setText("Draw!");
+ JOptionPane.showMessageDialog(this, "Draw!");
+ break;
+ case "OPPONENT_LEFT":
+ JOptionPane.showMessageDialog(this, "Opponent left the game.");
+ resetGame();
+ break;
+ case "ERROR:":
+ if (msg.contains("It is not your turn")) {
+ statusLabel.setText("Not your turn!");
+ } else {
+ // Start thinking again if move was invalid?
+ if (msg.contains("Invalid") || msg.contains("occupied")) {
+ makeAiMove(); // Retry
+ }
+ System.err.println("Server Error: " + msg);
+ }
+ break;
+ }
+ }
+
+ private void makeAiMove() {
+ new Thread(() -> {
+ try {
+ Thread.sleep(2000); // Wait 2s
+
+ String prompt = buildPrompt();
+ System.out.println("Sending Prompt to AI:\n" + prompt);
+
+ String response = callOpenRouter(prompt);
+ System.out.println("AI Response: " + response);
+
+ // Parse response: expecting row, col
+ // The prompt will ask specifically for "row col" format
+ String[] coords = parseCoordinates(response);
+ if (coords != null) {
+ send("MOVE " + coords[0] + " " + coords[1]);
+ } else {
+ System.err.println("Failed to parse AI move. Retrying random...");
+ makeRandomMove();
+ }
+
+ } catch (Exception e) {
+ e.printStackTrace();
+ makeRandomMove();
+ }
+ }).start();
+ }
+
+ private void makeRandomMove() {
+ // Fallback: pick first empty spot
+ for (int i = 0; i < 9; i++) {
+ if (boardState[i] == ' ') {
+ int r = i / 3;
+ int c = i % 3;
+ send("MOVE " + r + " " + c);
+ return;
+ }
+ }
+ }
+
+ private String buildPrompt() {
+ StringBuilder sb = new StringBuilder();
+ sb.append("Play Tic-Tac-Toe as '").append(mySymbol).append("'.\n");
+ sb.append("Board:\n");
+ for (int i = 0; i < 3; i++) {
+ for (int j = 0; j < 3; j++) {
+ char c = boardState[i * 3 + j];
+ sb.append(c == ' ' ? '_' : c).append(" ");
+ }
+ sb.append("\n");
+ }
+ sb.append(
+ "Respond ONLY with the coordinates of your next move in JSON format: {\"row\": <0-2>, \"col\": <0-2>}. Do not add any other text.");
+ return sb.toString();
+ }
+
+ private String callOpenRouter(String prompt) throws IOException, InterruptedException {
+ HttpClient client = HttpClient.newHttpClient();
+ String jsonBody = "{"
+ + "\"model\": \"" + MODEL + "\","
+ + "\"messages\": [{\"role\": \"user\", \"content\": \""
+ + prompt.replace("\n", "\\n").replace("\"", "\\\"") + "\"}],"
+ + "\"temperature\": 0.2" // Lower temperature for more deterministic/structured output
+ + "}";
+
+ HttpRequest request = HttpRequest.newBuilder()
+ .uri(java.net.URI.create("https://openrouter.ai/api/v1/chat/completions"))
+ .header("Authorization", "Bearer " + API_KEY)
+ .header("Content-Type", "application/json")
+ .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
+ .build();
+
+ HttpResponse response = client.send(request, HttpResponse.BodyHandlers.ofString());
+ String body = response.body();
+ // System.out.println("DEBUG RAW BODY: " + body); // excessive logging, but
+ // helpful if needed
+
+ // Parse content
+ int contentIndex = body.indexOf("\"content\":");
+ if (contentIndex != -1) {
+ int start = body.indexOf("\"", contentIndex + 10) + 1;
+ // Find the end quote, handling escaped quotes
+ int end = start;
+ while (true) {
+ end = body.indexOf("\"", end + 1);
+ if (body.charAt(end - 1) != '\\') {
+ break;
+ }
+ }
+ String content = body.substring(start, end);
+ return content.replace("\\n", "\n").replace("\\\"", "\"");
+ }
+ return "";
+ }
+
+ private String[] parseCoordinates(String text) {
+ // Look for JSON pattern first: "row": 1, "col": 2
+ Pattern jsonPattern = Pattern.compile("\"row\"\\s*:\\s*(\\d)\\s*,\\s*\"col\"\\s*:\\s*(\\d)");
+ Matcher m = jsonPattern.matcher(text);
+ if (m.find()) {
+ return new String[] { m.group(1), m.group(2) };
+ }
+
+ // Fallback: strictly row col numbers
+ m = Pattern.compile("(\\d)\\s+(\\d)").matcher(text);
+ if (m.find()) {
+ return new String[] { m.group(1), m.group(2) };
+ }
+
+ // One last fallback: just find any two digits
+ m = Pattern.compile("(\\d)[^\\d]+(\\d)").matcher(text);
+ if (m.find()) {
+ return new String[] { m.group(1), m.group(2) };
+ }
+
+ return null;
+ }
+
+ private void resetGame() {
+ cardLayout.show(mainPanel, "MENU");
+ for (JButton btn : buttons)
+ btn.setText("");
+ for (int i = 0; i < 9; i++)
+ boardState[i] = ' ';
+ statusLabel.setText("Connected.");
+ codeLabel.setText("Code: -");
+ }
+}
diff --git a/ai-client/target/ai-client-1.0-SNAPSHOT.jar b/ai-client/target/ai-client-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..0690cc5
Binary files /dev/null and b/ai-client/target/ai-client-1.0-SNAPSHOT.jar differ
diff --git a/ai-client/target/classes/com/lona/tictactoe/client/Main.class b/ai-client/target/classes/com/lona/tictactoe/client/Main.class
new file mode 100644
index 0000000..5cef921
Binary files /dev/null and b/ai-client/target/classes/com/lona/tictactoe/client/Main.class differ
diff --git a/ai-client/target/maven-archiver/pom.properties b/ai-client/target/maven-archiver/pom.properties
new file mode 100644
index 0000000..a77a248
--- /dev/null
+++ b/ai-client/target/maven-archiver/pom.properties
@@ -0,0 +1,5 @@
+#Generated by Maven
+#Tue Feb 10 14:17:06 CET 2026
+artifactId=ai-client
+groupId=com.lona.tictactoe
+version=1.0-SNAPSHOT
diff --git a/ai-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst b/ai-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
new file mode 100644
index 0000000..0128c4a
--- /dev/null
+++ b/ai-client/target/maven-status/maven-compiler-plugin/compile/default-compile/createdFiles.lst
@@ -0,0 +1 @@
+com/lona/tictactoe/client/Main.class
diff --git a/ai-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst b/ai-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
new file mode 100644
index 0000000..7812269
--- /dev/null
+++ b/ai-client/target/maven-status/maven-compiler-plugin/compile/default-compile/inputFiles.lst
@@ -0,0 +1 @@
+/home/collin/tictactoe/ai-client/src/main/java/com/lona/tictactoe/client/Main.java
diff --git a/ai-client/target/original-ai-client-1.0-SNAPSHOT.jar b/ai-client/target/original-ai-client-1.0-SNAPSHOT.jar
new file mode 100644
index 0000000..fbc03a5
Binary files /dev/null and b/ai-client/target/original-ai-client-1.0-SNAPSHOT.jar differ
diff --git a/server/src/main/java/com/lona/tictactoe/server/GameInstance.java b/server/src/main/java/com/lona/tictactoe/server/GameInstance.java
index c5b52b7..aa806fe 100644
--- a/server/src/main/java/com/lona/tictactoe/server/GameInstance.java
+++ b/server/src/main/java/com/lona/tictactoe/server/GameInstance.java
@@ -55,6 +55,7 @@ public class GameInstance {
if (checkWin(symbol)) {
finished = true;
+ lastWinner = symbol;
broadcast("WIN " + symbol);
} else if (checkDraw()) {
finished = true;
@@ -72,13 +73,32 @@ public class GameInstance {
char loser = (player == playerX) ? 'X' : 'O';
char winner = (loser == 'X') ? 'O' : 'X';
broadcast("WIN " + winner + " (Surrender)");
+ lastWinner = winner;
}
+ private char lastWinner = ' ';
+
public synchronized void restart() {
+ // Determine who starts: The loser of the previous game.
+ // If lastWinner was 'X', then 'O' (the loser) starts.
+ // If lastWinner was 'O', then 'X' (the loser) starts.
+ // If it was a draw or new game, default to 'X'.
+
+ if (lastWinner == 'X') {
+ currentTurn = 'O';
+ } else if (lastWinner == 'O') {
+ currentTurn = 'X';
+ } else {
+ currentTurn = 'X'; // Default for Draw/New
+ }
+
+ lastWinner = ' '; // Reset for next game
+ finished = false;
+
+ // Clear board
for (char[] row : board)
Arrays.fill(row, ' ');
- currentTurn = 'X';
- finished = false;
+
broadcast("RESTART");
sendBoard();
broadcast("TURN " + currentTurn);
diff --git a/server/target/classes/com/lona/tictactoe/server/GameInstance.class b/server/target/classes/com/lona/tictactoe/server/GameInstance.class
index 90d6e32..5c4a86e 100644
Binary files a/server/target/classes/com/lona/tictactoe/server/GameInstance.class and b/server/target/classes/com/lona/tictactoe/server/GameInstance.class differ
diff --git a/server/target/maven-archiver/pom.properties b/server/target/maven-archiver/pom.properties
index 2bfbc85..2b8e22e 100644
--- a/server/target/maven-archiver/pom.properties
+++ b/server/target/maven-archiver/pom.properties
@@ -1,5 +1,5 @@
#Generated by Maven
-#Tue Feb 10 14:07:14 CET 2026
+#Tue Feb 10 14:16:42 CET 2026
artifactId=server
groupId=com.lona.tictactoe
version=1.0-SNAPSHOT
diff --git a/server/target/original-server-1.0-SNAPSHOT.jar b/server/target/original-server-1.0-SNAPSHOT.jar
index 6cfe39a..87d724b 100644
Binary files a/server/target/original-server-1.0-SNAPSHOT.jar and b/server/target/original-server-1.0-SNAPSHOT.jar differ
diff --git a/server/target/server-1.0-SNAPSHOT.jar b/server/target/server-1.0-SNAPSHOT.jar
index ba14234..c46933b 100644
Binary files a/server/target/server-1.0-SNAPSHOT.jar and b/server/target/server-1.0-SNAPSHOT.jar differ
diff --git a/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java b/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java
index 39efa55..9c13fa8 100644
--- a/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java
+++ b/web-client/src/main/java/com/lona/tictactoe/web/TcpSession.java
@@ -30,9 +30,11 @@ public class TcpSession extends Thread {
@Override
public void run() {
try {
+ System.out.println("TcpSession: Connecting to " + host + ":" + port);
socket = new Socket(host, port);
out = new PrintWriter(socket.getOutputStream(), true);
in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
+ System.out.println("TcpSession: Connected successfully");
// Flush queue
String queued;
@@ -46,8 +48,16 @@ public class TcpSession extends Thread {
session.sendMessage(new TextMessage(line));
}
}
+ } catch (java.net.ConnectException ce) {
+ System.err.println("TcpSession: Failed to connect to game server: " + ce.getMessage());
+ } catch (java.net.SocketException se) {
+ if (running) {
+ // Only print if we didn't initiate the close
+ System.err.println("TcpSession: Socket closed unexpectedly: " + se.getMessage());
+ se.printStackTrace();
+ }
} catch (IOException e) {
- // connection lost or failed
+ System.err.println("TcpSession: IO Error: " + e.getMessage());
e.printStackTrace();
} finally {
close();
diff --git a/web-client/target/classes/com/lona/tictactoe/web/TcpSession.class b/web-client/target/classes/com/lona/tictactoe/web/TcpSession.class
index 95f3714..aa89f8e 100644
Binary files a/web-client/target/classes/com/lona/tictactoe/web/TcpSession.class and b/web-client/target/classes/com/lona/tictactoe/web/TcpSession.class differ
diff --git a/web-client/target/web-client-1.0-SNAPSHOT.jar b/web-client/target/web-client-1.0-SNAPSHOT.jar
index 3890a40..7d7de80 100644
Binary files a/web-client/target/web-client-1.0-SNAPSHOT.jar and b/web-client/target/web-client-1.0-SNAPSHOT.jar differ
diff --git a/web-client/target/web-client-1.0-SNAPSHOT.jar.original b/web-client/target/web-client-1.0-SNAPSHOT.jar.original
index 9cff0d9..957ff94 100644
Binary files a/web-client/target/web-client-1.0-SNAPSHOT.jar.original and b/web-client/target/web-client-1.0-SNAPSHOT.jar.original differ