This commit is contained in:
Hymmel 2026-02-10 14:41:19 +01:00
parent 1cb5c0923b
commit c117b13ddc
11 changed files with 208 additions and 14 deletions

View file

@ -279,33 +279,133 @@ public class Main extends JFrame {
private void makeAiMove() { private void makeAiMove() {
new Thread(() -> { new Thread(() -> {
try { 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(); String prompt = buildPrompt();
System.out.println("Sending Prompt to AI:\n" + prompt); System.out.println("Sending Prompt to AI...");
String response = callOpenRouter(prompt); String response = callOpenRouter(prompt);
System.out.println("AI Response: " + response); System.out.println("AI Response: " + response);
// Parse response: expecting row, col
// The prompt will ask specifically for "row col" format
String[] coords = parseCoordinates(response); String[] coords = parseCoordinates(response);
if (coords != null) { 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 { } else {
System.err.println("Failed to parse AI move. Retrying random..."); System.err.println("Failed to parse AI move. Using Minimax fallback.");
makeRandomMove(); makeSmartMove();
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
makeRandomMove(); makeSmartMove();
} }
}).start(); }).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() { private void makeRandomMove() {
// Fallback: pick first empty spot
for (int i = 0; i < 9; i++) { for (int i = 0; i < 9; i++) {
if (boardState[i] == ' ') { if (boardState[i] == ' ') {
int r = i / 3; int r = i / 3;
@ -318,17 +418,19 @@ public class Main extends JFrame {
private String buildPrompt() { private String buildPrompt() {
StringBuilder sb = new StringBuilder(); StringBuilder sb = new StringBuilder();
sb.append("Play Tic-Tac-Toe as '").append(mySymbol).append("'.\n"); sb.append("You are an unbeatable Tic-Tac-Toe AI. You are player '").append(mySymbol).append("'.\n");
sb.append("Board:\n"); sb.append("Current Board State (row, col):\n");
for (int i = 0; i < 3; i++) { for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) { for (int j = 0; j < 3; j++) {
char c = boardState[i * 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("\n");
} }
sb.append( 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\": <row>, \"col\": <col>}");
return sb.toString(); return sb.toString();
} }

View file

@ -1,5 +1,5 @@
#Generated by Maven #Generated by Maven
#Tue Feb 10 14:17:06 CET 2026 #Tue Feb 10 14:20:59 CET 2026
artifactId=ai-client artifactId=ai-client
groupId=com.lona.tictactoe groupId=com.lona.tictactoe
version=1.0-SNAPSHOT version=1.0-SNAPSHOT

View file

@ -113,6 +113,46 @@ function resetGame() {
statusDiv.style.color = '#10b981'; statusDiv.style.color = '#10b981';
} }
const visibleDebug = document.getElementById('visible-debug');
function log(text) {
if (!visibleDebug) return;
const now = new Date().toLocaleTimeString();
visibleDebug.innerHTML += `<div>[${now}] ${text}</div>`;
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) { function updateBoard(boardStr) {
cells.forEach((cell, i) => { cells.forEach((cell, i) => {
if (i < boardStr.length) { if (i < boardStr.length) {

View file

@ -58,7 +58,13 @@
<button id="leave-btn" class="btn secondary">Leave Lobby</button> <button id="leave-btn" class="btn secondary">Leave Lobby</button>
<button id="surrender-btn" class="btn danger">Surrender</button> <button id="surrender-btn" class="btn danger">Surrender</button>
</div> </div>
<div id="debug-log"
style="margin-top: 20px; font-size: 0.8rem; color: #555; text-align: left; max-height: 200px; overflow-y: auto; border: 1px solid #333; padding: 10px; display: none;">
</div>
</div> </div>
<div id="visible-debug" style="margin-top: 20px; color: #666; font-size: 0.8rem;"></div>
</div> </div>
<script src="/js/app.js"></script> <script src="/js/app.js"></script>

View file

@ -113,6 +113,46 @@ function resetGame() {
statusDiv.style.color = '#10b981'; statusDiv.style.color = '#10b981';
} }
const visibleDebug = document.getElementById('visible-debug');
function log(text) {
if (!visibleDebug) return;
const now = new Date().toLocaleTimeString();
visibleDebug.innerHTML += `<div>[${now}] ${text}</div>`;
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) { function updateBoard(boardStr) {
cells.forEach((cell, i) => { cells.forEach((cell, i) => {
if (i < boardStr.length) { if (i < boardStr.length) {

View file

@ -58,7 +58,13 @@
<button id="leave-btn" class="btn secondary">Leave Lobby</button> <button id="leave-btn" class="btn secondary">Leave Lobby</button>
<button id="surrender-btn" class="btn danger">Surrender</button> <button id="surrender-btn" class="btn danger">Surrender</button>
</div> </div>
<div id="debug-log"
style="margin-top: 20px; font-size: 0.8rem; color: #555; text-align: left; max-height: 200px; overflow-y: auto; border: 1px solid #333; padding: 10px; display: none;">
</div>
</div> </div>
<div id="visible-debug" style="margin-top: 20px; color: #666; font-size: 0.8rem;"></div>
</div> </div>
<script src="/js/app.js"></script> <script src="/js/app.js"></script>