This commit is contained in:
Hymmel 2026-02-10 14:17:25 +01:00
parent 0937fc35de
commit 1cb5c0923b
18 changed files with 496 additions and 4 deletions

14
ai-client/build.sh Executable file
View file

@ -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

35
ai-client/pom.xml Normal file
View file

@ -0,0 +1,35 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.lona.tictactoe</groupId>
<artifactId>ai-client</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>17</maven.compiler.source>
<maven.compiler.target>17</maven.compiler.target>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.5.0</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.lona.tictactoe.client.Main</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View file

@ -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<String> 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: -");
}
}

Binary file not shown.

View file

@ -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

View file

@ -0,0 +1 @@
com/lona/tictactoe/client/Main.class

View file

@ -0,0 +1 @@
/home/collin/tictactoe/ai-client/src/main/java/com/lona/tictactoe/client/Main.java

Binary file not shown.

View file

@ -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);

View file

@ -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

View file

@ -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();