w
This commit is contained in:
parent
1cb5c0923b
commit
c117b13ddc
11 changed files with 208 additions and 14 deletions
|
|
@ -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\": <row>, \"col\": <col>}");
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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 += `<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) {
|
||||
cells.forEach((cell, i) => {
|
||||
if (i < boardStr.length) {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,13 @@
|
|||
<button id="leave-btn" class="btn secondary">Leave Lobby</button>
|
||||
<button id="surrender-btn" class="btn danger">Surrender</button>
|
||||
</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 id="visible-debug" style="margin-top: 20px; color: #666; font-size: 0.8rem;"></div>
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
|
|||
|
|
@ -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 += `<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) {
|
||||
cells.forEach((cell, i) => {
|
||||
if (i < boardStr.length) {
|
||||
|
|
|
|||
|
|
@ -58,7 +58,13 @@
|
|||
<button id="leave-btn" class="btn secondary">Leave Lobby</button>
|
||||
<button id="surrender-btn" class="btn danger">Surrender</button>
|
||||
</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 id="visible-debug" style="margin-top: 20px; color: #666; font-size: 0.8rem;"></div>
|
||||
</div>
|
||||
|
||||
<script src="/js/app.js"></script>
|
||||
|
|
|
|||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue