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() {
|
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 {
|
} else {
|
||||||
System.err.println("Failed to parse AI move. Retrying random...");
|
System.err.println("AI suggested invalid move. Using Minimax fallback.");
|
||||||
makeRandomMove();
|
makeSmartMove();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
System.err.println("Failed to parse AI move. Using Minimax fallback.");
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -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
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,15 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
<div id="visible-debug" style="margin-top: 20px; color: #666; font-size: 0.8rem;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -58,9 +58,15 @@
|
||||||
<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>
|
||||||
|
|
||||||
|
<div id="visible-debug" style="margin-top: 20px; color: #666; font-size: 0.8rem;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="/js/app.js"></script>
|
<script src="/js/app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
Loading…
Add table
Reference in a new issue