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 index b94e4d3..6fad480 100644 --- a/ai-client/src/main/java/com/lona/tictactoe/client/Main.java +++ b/ai-client/src/main/java/com/lona/tictactoe/client/Main.java @@ -279,33 +279,133 @@ public class Main extends JFrame { private void makeAiMove() { new Thread(() -> { try { - Thread.sleep(2000); // Wait 2s + Thread.sleep(1500); // Slightly faster + + // 1. Try Simple Rule-Based / Minimax first (guarantees better play) + // If we want "AI" feel, we can use LLM, but for "winning", logic is better. + // Let's mix them. Use LLM but with VERY specific instructions, + // and if it fails or returns invalid, fall back to smart move. String prompt = buildPrompt(); - System.out.println("Sending Prompt to AI:\n" + prompt); + System.out.println("Sending Prompt to AI..."); 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]); + int r = Integer.parseInt(coords[0]); + int c = Integer.parseInt(coords[1]); + // Validate move locally before sending + if (isValidMove(r, c)) { + send("MOVE " + r + " " + c); + } else { + System.err.println("AI suggested invalid move. Using Minimax fallback."); + makeSmartMove(); + } } else { - System.err.println("Failed to parse AI move. Retrying random..."); - makeRandomMove(); + System.err.println("Failed to parse AI move. Using Minimax fallback."); + makeSmartMove(); } } catch (Exception e) { e.printStackTrace(); - makeRandomMove(); + makeSmartMove(); } }).start(); } + private boolean isValidMove(int r, int c) { + if (r < 0 || r > 2 || c < 0 || c > 2) + return false; + return boardState[r * 3 + c] == ' '; + } + + private void makeSmartMove() { + // Simple Minimax or Rule-based + int bestScore = Integer.MIN_VALUE; + int move = -1; + + // Convert boardState to something easier + char[] board = boardState.clone(); + + for (int i = 0; i < 9; i++) { + if (board[i] == ' ') { + board[i] = mySymbol; + int score = minimax(board, 0, false); + board[i] = ' '; + if (score > bestScore) { + bestScore = score; + move = i; + } + } + } + + if (move != -1) { + int r = move / 3; + int c = move % 3; + send("MOVE " + r + " " + c); + } else { + makeRandomMove(); + } + } + + private char getOpponentSymbol() { + return (mySymbol == 'X') ? 'O' : 'X'; + } + + private int minimax(char[] board, int depth, boolean isMaximizing) { + char result = checkWinner(board); + if (result == mySymbol) + return 10 - depth; + if (result == getOpponentSymbol()) + return depth - 10; + if (result == 'D') + return 0; // Draw + + if (isMaximizing) { + int bestScore = Integer.MIN_VALUE; + for (int i = 0; i < 9; i++) { + if (board[i] == ' ') { + board[i] = mySymbol; + int score = minimax(board, depth + 1, false); + board[i] = ' '; + bestScore = Math.max(score, bestScore); + } + } + return bestScore; + } else { + int bestScore = Integer.MAX_VALUE; + for (int i = 0; i < 9; i++) { + if (board[i] == ' ') { + board[i] = getOpponentSymbol(); + int score = minimax(board, depth + 1, true); + board[i] = ' '; + bestScore = Math.min(score, bestScore); + } + } + return bestScore; + } + } + + private char checkWinner(char[] b) { + int[][] wins = { + { 0, 1, 2 }, { 3, 4, 5 }, { 6, 7, 8 }, // rows + { 0, 3, 6 }, { 1, 4, 7 }, { 2, 5, 8 }, // cols + { 0, 4, 8 }, { 2, 4, 6 } // diags + }; + for (int[] w : wins) { + if (b[w[0]] != ' ' && b[w[0]] == b[w[1]] && b[w[1]] == b[w[2]]) { + return b[w[0]]; + } + } + for (int i = 0; i < 9; i++) + if (b[i] == ' ') + return ' '; // Not finished + return 'D'; // Draw + } + private void makeRandomMove() { - // Fallback: pick first empty spot for (int i = 0; i < 9; i++) { if (boardState[i] == ' ') { int r = i / 3; @@ -318,17 +418,19 @@ public class Main extends JFrame { private String buildPrompt() { StringBuilder sb = new StringBuilder(); - sb.append("Play Tic-Tac-Toe as '").append(mySymbol).append("'.\n"); - sb.append("Board:\n"); + sb.append("You are an unbeatable Tic-Tac-Toe AI. You are player '").append(mySymbol).append("'.\n"); + sb.append("Current Board State (row, col):\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("[").append(i).append(",").append(j).append("]=").append(c == ' ' ? "EMPTY" : 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."); + "\nYour goal is to WIN. If you cannot win immediately, BLOCK the opponent. If neither, play optimally to force a draw.\n"); + sb.append("Analyze the board carefully. Don't make random moves.\n"); + sb.append("Respond with JSON ONLY: {\"row\": , \"col\": }"); return sb.toString(); } diff --git a/ai-client/target/ai-client-1.0-SNAPSHOT.jar b/ai-client/target/ai-client-1.0-SNAPSHOT.jar index 0690cc5..b0f18f2 100644 Binary files a/ai-client/target/ai-client-1.0-SNAPSHOT.jar 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 index 5cef921..594ace8 100644 Binary files a/ai-client/target/classes/com/lona/tictactoe/client/Main.class 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 index a77a248..d1e84c7 100644 --- a/ai-client/target/maven-archiver/pom.properties +++ b/ai-client/target/maven-archiver/pom.properties @@ -1,5 +1,5 @@ #Generated by Maven -#Tue Feb 10 14:17:06 CET 2026 +#Tue Feb 10 14:20:59 CET 2026 artifactId=ai-client groupId=com.lona.tictactoe version=1.0-SNAPSHOT diff --git a/ai-client/target/original-ai-client-1.0-SNAPSHOT.jar b/ai-client/target/original-ai-client-1.0-SNAPSHOT.jar index fbc03a5..e336dd1 100644 Binary files a/ai-client/target/original-ai-client-1.0-SNAPSHOT.jar and b/ai-client/target/original-ai-client-1.0-SNAPSHOT.jar differ diff --git a/web-client/src/main/resources/static/js/app.js b/web-client/src/main/resources/static/js/app.js index b3cd91f..f303fd3 100644 --- a/web-client/src/main/resources/static/js/app.js +++ b/web-client/src/main/resources/static/js/app.js @@ -113,6 +113,46 @@ function resetGame() { statusDiv.style.color = '#10b981'; } +const visibleDebug = document.getElementById('visible-debug'); + +function log(text) { + if (!visibleDebug) return; + const now = new Date().toLocaleTimeString(); + visibleDebug.innerHTML += `
[${now}] ${text}
`; + console.log(text); +} + +function connect() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = `${protocol}//${window.location.host}/game`; + log(`Connecting to WebSocket: ${url}`); + + ws = new WebSocket(url); + + ws.onopen = () => { + log('WebSocket: OnOpen fired. Connected.'); + statusDiv.textContent = 'Connected to Server'; + statusDiv.style.color = '#10b981'; + }; + + ws.onclose = (event) => { + log(`WebSocket: OnClose fired. Code: ${event.code}, Reason: ${event.reason}`); + statusDiv.textContent = 'Disconnected. Reconnecting...'; + statusDiv.style.color = '#ef4444'; + setTimeout(connect, 2000); + }; + + ws.onerror = (error) => { + log(`WebSocket: OnError fired. State: ${ws.readyState}`); + console.error("WebSocket Error:", error); + }; + + ws.onmessage = (event) => { + // log(`Received: ${event.data}`); // don't log every message to avoid spam + handleMessage(event.data); + }; +} + function updateBoard(boardStr) { cells.forEach((cell, i) => { if (i < boardStr.length) { diff --git a/web-client/src/main/resources/templates/index.html b/web-client/src/main/resources/templates/index.html index 89a0d3d..0fd135c 100644 --- a/web-client/src/main/resources/templates/index.html +++ b/web-client/src/main/resources/templates/index.html @@ -58,7 +58,13 @@ + + + +
diff --git a/web-client/target/classes/static/js/app.js b/web-client/target/classes/static/js/app.js index b3cd91f..f303fd3 100644 --- a/web-client/target/classes/static/js/app.js +++ b/web-client/target/classes/static/js/app.js @@ -113,6 +113,46 @@ function resetGame() { statusDiv.style.color = '#10b981'; } +const visibleDebug = document.getElementById('visible-debug'); + +function log(text) { + if (!visibleDebug) return; + const now = new Date().toLocaleTimeString(); + visibleDebug.innerHTML += `
[${now}] ${text}
`; + console.log(text); +} + +function connect() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const url = `${protocol}//${window.location.host}/game`; + log(`Connecting to WebSocket: ${url}`); + + ws = new WebSocket(url); + + ws.onopen = () => { + log('WebSocket: OnOpen fired. Connected.'); + statusDiv.textContent = 'Connected to Server'; + statusDiv.style.color = '#10b981'; + }; + + ws.onclose = (event) => { + log(`WebSocket: OnClose fired. Code: ${event.code}, Reason: ${event.reason}`); + statusDiv.textContent = 'Disconnected. Reconnecting...'; + statusDiv.style.color = '#ef4444'; + setTimeout(connect, 2000); + }; + + ws.onerror = (error) => { + log(`WebSocket: OnError fired. State: ${ws.readyState}`); + console.error("WebSocket Error:", error); + }; + + ws.onmessage = (event) => { + // log(`Received: ${event.data}`); // don't log every message to avoid spam + handleMessage(event.data); + }; +} + function updateBoard(boardStr) { cells.forEach((cell, i) => { if (i < boardStr.length) { diff --git a/web-client/target/classes/templates/index.html b/web-client/target/classes/templates/index.html index 89a0d3d..0fd135c 100644 --- a/web-client/target/classes/templates/index.html +++ b/web-client/target/classes/templates/index.html @@ -58,7 +58,13 @@ + + + +
diff --git a/web-client/target/web-client-1.0-SNAPSHOT.jar b/web-client/target/web-client-1.0-SNAPSHOT.jar index 7d7de80..44d4000 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 957ff94..d397732 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