const express = require("express"); const http = require("http"); const net = require("net"); // Added for Bot Support const WebSocket = require("ws"); const app = express(); app.use(express.static("public")); const server = http.createServer(app); const wss = new WebSocket.Server({ server }); // Board Logic const board_wolf = { 0: [1, 2, 3], 1: [0, 2, 4, 5], 2: [0, 1, 3, 5], 3: [0, 2, 5, 6], 4: [1, 5, 7], 5: [1, 2, 3, 4, 6, 7, 8, 9], 6: [3, 5, 9], 7: [4, 5, 8, 10], 8: [5, 7, 9, 10], 9: [5, 6, 8, 10], 10: [7, 8, 9] }; const board_sheep = { 0: [1, 2, 3], 1: [2, 4, 5], 2: [1, 3, 5], 3: [2, 5, 6], 4: [5, 7], 5: [4, 6, 7, 8, 9], 6: [5, 9], 7: [8, 10], 8: [7, 9, 10], 9: [8, 10], 10: [] }; // Direction Map for Bots const pos_coords = { 0: [0,1], 1: [1,0], 2: [1,1], 3: [1,2], 4: [2,0], 5: [2,1], 6: [2,2], 7: [3,0], 8: [3,1], 9: [3,2], 10: [4,1] }; const dir_map = { "n": [0,-1], "s": [0,1], "o": [1,0], "w": [-1,0], "no": [1,-1], "nw": [-1,-1], "so": [1,1], "sw": [-1,1] }; function getDir(from, to) { const dX = pos_coords[to][0] - pos_coords[from][0]; const dY = pos_coords[to][1] - pos_coords[from][1]; return Object.keys(dir_map).find(k => dir_map[k][0] === dX && dir_map[k][1] === dY); } const rooms = {}; // --- BOT TCP SERVER (Port 3031) --- const botServer = net.createServer((socket) => { let roomId = `BOT_${Math.random().toString(36).substring(7)}`; let botName = "UnknownBot"; socket.write("SEND: \n"); socket.on("data", (data) => { const msg = data.toString().trim(); // Initial Handshake if (!rooms[roomId]) { botName = msg.split(" ").pop(); // socket.write(`#welcome ${botName}\n`); rooms[roomId] = { game: { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null, lastMove: null }, players: [{ role: "wolf", isBot: true, socket: socket }], // Bot defaults to Wolf nextFirstRole: "sheep" }; broadcastRoomList(); return; } const room = rooms[roomId]; const bot = room.players.find(p => p.isBot); const game = room.game; if (game.turn !== bot.role || game.winner) return; let from, to; if (bot.role === "wolf") { from = game.wolf; const targetDir = msg.toLowerCase(); to = board_wolf[from].find(t => getDir(from, t) === targetDir); } else { // FIX: Parse the first character as the sheep index (1-3) const sheepIndex = parseInt(msg[0]) - 1; // Convert 1-3 to 0-2 array index const targetDir = msg.substring(1).toLowerCase(); // Ensure the index is valid (0, 1, or 2) if (game.sheep[sheepIndex] !== undefined) { from = game.sheep[sheepIndex]; // Get board position (e.g., 6) to = board_sheep[from].find(t => getDir(from, t) === targetDir); } } if (to !== undefined) { handleMove(roomId, bot.role, { from, to }); } }); socket.on("close", () => { delete rooms[roomId]; broadcastRoomList(); }); }); function swapRoles(room) { room.players.forEach(p => { p.role = (p.role === "wolf") ? "sheep" : "wolf"; if (p.isBot) p.botInitialized = false; // Reset bot flag for new role msg }); } function sendBotState(roomId) { const room = rooms[roomId]; const bot = room.players.find(p => p.isBot); if (!bot || !bot.socket) return; const game = room.game; // 1. Handle Game End if (game.winner) { const result = (game.winner === bot.role) ? "you win" : "you loose"; bot.socket.write(`#gameends ${result}\n`); return; } // 2. Send Role (Only once at start of game/restart) if (!bot.botInitialized) { bot.socket.write(`#role ${bot.role === "wolf" ? "WOLF" : "SCHAF"}\n`); bot.botInitialized = true; } // 3. Send Turn Data if (game.turn === bot.role) { bot.socket.write(`#pos_opponent ${bot.role === "wolf" ? game.sheep.join(" ") : game.wolf}\n`); bot.socket.write(`#pos_you ${bot.role === "wolf" ? game.wolf : game.sheep.join(" ")}\n`); let moves = []; if (bot.role === "wolf") { board_wolf[game.wolf].forEach(t => { if (!game.sheep.includes(t)) moves.push(getDir(game.wolf, t)); }); } else { game.sheep.forEach(s => { board_sheep[s].forEach(t => { if (t !== game.wolf && !game.sheep.includes(t)) moves.push(`${s}${getDir(s, t)}`); }); }); } bot.socket.write(`#moves ${moves.join(" ")}\n`); } } // --- EXISTING LOGIC UPDATED --- function broadcastRoomList() { const list = JSON.stringify({ type: "room_list", rooms: getRoomList() }); wss.clients.forEach(client => { if (client.readyState === WebSocket.OPEN && !client.roomId) client.send(list); }); } function getRoomList() { return Object.keys(rooms).map(id => ({ id, players: rooms[id].players.length, isBotRoom: rooms[id].players.some(p => p.isBot) })); } function handleMove(roomId, role, { from, to }) { const room = rooms[roomId]; if (!room || room.game.winner || room.game.turn !== role) return; const game = room.game; const allowed = (role === "wolf") ? board_wolf[from] : board_sheep[from]; if (!allowed?.includes(to) || game.sheep.includes(to) || game.wolf === to) return; if (role === "wolf") { game.wolf = to; game.turn = "sheep"; } else { game.sheep[game.sheep.indexOf(from)] = to; game.turn = "wolf"; } game.moveCount++; game.winner = checkWinConditions(game); broadcastState(roomId); if (room.players.some(p => p.isBot)) sendBotState(roomId); } function broadcastState(roomId) { const room = rooms[roomId]; if (!room) return; room.players.forEach(p => { if (p.ws?.readyState === WebSocket.OPEN) { p.ws.send(JSON.stringify({ type: "state", game: room.game, yourRole: p.role, onlineCount: room.players.length })); } }); } function checkWinConditions(game) { if (game.wolf === 0 || game.moveCount >= 40) return "wolf"; const canWolfMove = board_wolf[game.wolf].some(to => !game.sheep.includes(to)); if (!canWolfMove) return "sheep"; return null; } // WebSocket connection logic remains mostly same, just ensure it adds human to bot room wss.on("connection", (ws, req) => { const urlParams = new URLSearchParams(req.url.split('?')[1]); let roomId = urlParams.get("room"); let sessionId = urlParams.get("sessionId"); if (!roomId || roomId === "null") { ws.send(JSON.stringify({ type: "room_list", rooms: getRoomList() })); return; } if (!rooms[roomId]) { rooms[roomId] = { game: { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null, lastMove: null }, players: [], nextFirstRole: "sheep" }; } const room = rooms[roomId]; ws.roomId = roomId; ws.sessionId = sessionId; // Store on socket for easy lookup during close let player = room.players.find(p => p.sessionId === sessionId); if (player) { player.ws = ws; } else if (room.players.length < 2) { let role; if (room.players.length === 1) { role = (room.players[0].role === "wolf") ? "sheep" : "wolf"; } else { role = "sheep"; } player = { ws, role, sessionId, isBot: false }; room.players.push(player); } ws.send(JSON.stringify({ type: "init", role: player.role, roomId })); broadcastRoomList(); broadcastState(roomId); if (room.players.some(p => p.isBot)) sendBotState(roomId); // --- HANDLE DISCONNECT --- ws.on("close", () => { const currentRoom = rooms[ws.roomId]; if (!currentRoom) return; // Remove the player from the room array currentRoom.players = currentRoom.players.filter(p => p.sessionId !== ws.sessionId); // If the room is now empty (or only contains a bot), delete it const humanRemaining = currentRoom.players.some(p => !p.isBot); if (currentRoom.players.length === 0 || (!humanRemaining && currentRoom.players.some(p => p.isBot))) { // If it was a bot room, close the bot socket before deleting const bot = currentRoom.players.find(p => p.isBot); if (bot && bot.socket) bot.socket.destroy(); delete rooms[ws.roomId]; } else { // Room still has someone, notify them broadcastState(ws.roomId); } broadcastRoomList(); }); ws.on("message", msg => { let data = JSON.parse(msg); const playerObj = room.players.find(p => p.sessionId === sessionId); if (!playerObj) return; if (data.type === "restart") { swapRoles(room); room.game = { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null, lastMove: null }; room.players.forEach(p => { if (p.ws && p.ws.readyState === WebSocket.OPEN) { p.ws.send(JSON.stringify({ type: "init", role: p.role, roomId })); } }); broadcastState(roomId); if (room.players.some(p => p.isBot)) sendBotState(roomId); } else { handleMove(roomId, playerObj.role, data); } }); }); server.listen(3030, () => console.log("Web Server running on 3030")); botServer.listen(3031, () => console.log("Bot TCP Server running on 3031"));