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