diff --git a/Dockerfile b/Dockerfile index 54571e1..d2b38a3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -4,4 +4,5 @@ COPY package.json . RUN npm install COPY . . EXPOSE 3030 -CMD ["node","server.js"] +EXPOSE 3031 +CMD ["node","server.js"] \ No newline at end of file diff --git a/public/index.html b/public/index.html index 4ea08eb..ab90475 100644 --- a/public/index.html +++ b/public/index.html @@ -467,6 +467,7 @@ const msg = JSON.parse(e.data); if (msg.type === "room_list") updateRoomList(msg.rooms); if (msg.type === "init") { + myRole = msg.role; roomId = msg.roomId; document.getElementById("roomCode").innerText = roomId.toUpperCase(); window.history.replaceState({}, '', `?room=${roomId}`); diff --git a/server.js b/server.js index 253daf6..6a0ece6 100644 --- a/server.js +++ b/server.js @@ -1,7 +1,7 @@ const express = require("express"); const http = require("http"); +const net = require("net"); // Added for Bot Support const WebSocket = require("ws"); -const { randomBytes } = require("crypto"); const app = express(); app.use(express.static("public")); @@ -9,28 +9,177 @@ 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; + + // Handle Bot Move (e.g., "nw" or "3o") + 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 { + const sheepId = parseInt(msg[0]); + const targetDir = msg.substring(1).toLowerCase(); + from = sheepId; + 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, - turn: rooms[id].game.turn + isBotRoom: rooms[id].players.some(p => p.isBot) })); } -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 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"); @@ -52,83 +201,55 @@ wss.on("connection", (ws, req) => { ws.roomId = roomId; let player = room.players.find(p => p.sessionId === sessionId); - if (!player && room.players.length >= 2) { - ws.send(JSON.stringify({ type: "full" })); - return; - } - + if (player) { - if (player.timeout) clearTimeout(player.timeout); + // Reconnecting existing player player.ws = ws; - } else { - const role = room.players.length === 0 ? room.nextFirstRole : (room.nextFirstRole === "sheep" ? "wolf" : "sheep"); - player = { ws, role, sessionId, timeout: null }; + } else if (room.players.length < 2) { + // New player joining: logic to pick the opposite role + let role; + if (room.players.length === 1) { + // Pick the role that isn't taken + const takenRole = room.players[0].role; + role = (takenRole === "wolf") ? "sheep" : "wolf"; + } else { + // First player in the room defaults to sheep + role = "sheep"; + } + + player = { ws, role, sessionId }; room.players.push(player); } + // Inform the client of their assigned role ws.send(JSON.stringify({ type: "init", role: player.role, roomId })); + broadcastRoomList(); broadcastState(roomId); + + // If a bot is involved, update its state + if (room.players.some(p => p.isBot)) sendBotState(roomId); ws.on("message", msg => { let data = JSON.parse(msg); if (data.type === "restart") { - room.players.forEach(p => p.role = (p.role === "wolf" ? "sheep" : "wolf")); - room.nextFirstRole = room.nextFirstRole === "sheep" ? "wolf" : "sheep"; + swapRoles(room); room.game = { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null, lastMove: null }; - broadcastState(roomId); - } else if (data.from !== undefined && data.to !== undefined) { - handleMove(roomId, player.role, data); - } - }); - - ws.on("close", () => { - if (player) { - player.timeout = setTimeout(() => { - room.players = room.players.filter(p => p.sessionId !== sessionId); - if (room.players.length === 0) { - delete rooms[roomId]; + + // Critical: Re-sync roles for all human clients on restart + room.players.forEach(p => { + if (p.ws && p.ws.readyState === WebSocket.OPEN) { + p.ws.send(JSON.stringify({ type: "init", role: p.role, roomId })); } - broadcastRoomList(); - broadcastState(roomId); - }, 1000); + }); + + broadcastState(roomId); + if (room.players.some(p => p.isBot)) sendBotState(roomId); + } else { + handleMove(roomId, player.role, data); } }); }); -function broadcastState(roomId) { - const room = rooms[roomId]; - if (!room) return; - const online = room.players.filter(p => p.ws?.readyState === WebSocket.OPEN).length; - room.players.forEach(p => { - if (p.ws?.readyState === WebSocket.OPEN) { - p.ws.send(JSON.stringify({ type: "state", game: room.game, yourRole: p.role, onlineCount: online })); - } - }); -} - -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.lastMove = { from, to, role }; - game.winner = checkWinConditions(game); - broadcastState(roomId); -} - -function checkWinConditions(game) { - if (game.wolf === 0) return "wolf"; - if (game.moveCount >= 40) return "wolf"; - const canWolfMove = board_wolf[game.wolf].some(to => !game.sheep.includes(to)); - if (!canWolfMove) return "sheep"; - return null; -} - -server.listen(3030, () => console.log("Server running on 3030")); \ No newline at end of file +server.listen(3030, () => console.log("Web Server running on 3030")); +botServer.listen(3031, () => console.log("Bot TCP Server running on 3031")); \ No newline at end of file diff --git a/start.sh b/start.sh index a4eab50..8858f1a 100755 --- a/start.sh +++ b/start.sh @@ -1,4 +1,4 @@ docker stop wolf-sheep-container docker rm wolf-sheep-container docker build -t wolf-sheep . -docker run -d -p 3030:3030 --name wolf-sheep-container wolf-sheep +docker run -d -p 3030:3030 -p 3031:3031 --name wolf-sheep-container wolf-sheep