Bot server support
This commit is contained in:
@@ -4,4 +4,5 @@ COPY package.json .
|
|||||||
RUN npm install
|
RUN npm install
|
||||||
COPY . .
|
COPY . .
|
||||||
EXPOSE 3030
|
EXPOSE 3030
|
||||||
|
EXPOSE 3031
|
||||||
CMD ["node","server.js"]
|
CMD ["node","server.js"]
|
||||||
@@ -467,6 +467,7 @@
|
|||||||
const msg = JSON.parse(e.data);
|
const msg = JSON.parse(e.data);
|
||||||
if (msg.type === "room_list") updateRoomList(msg.rooms);
|
if (msg.type === "room_list") updateRoomList(msg.rooms);
|
||||||
if (msg.type === "init") {
|
if (msg.type === "init") {
|
||||||
|
myRole = msg.role;
|
||||||
roomId = msg.roomId;
|
roomId = msg.roomId;
|
||||||
document.getElementById("roomCode").innerText = roomId.toUpperCase();
|
document.getElementById("roomCode").innerText = roomId.toUpperCase();
|
||||||
window.history.replaceState({}, '', `?room=${roomId}`);
|
window.history.replaceState({}, '', `?room=${roomId}`);
|
||||||
|
|||||||
255
server.js
255
server.js
@@ -1,7 +1,7 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
const net = require("net"); // Added for Bot Support
|
||||||
const WebSocket = require("ws");
|
const WebSocket = require("ws");
|
||||||
const { randomBytes } = require("crypto");
|
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.static("public"));
|
app.use(express.static("public"));
|
||||||
@@ -9,28 +9,177 @@ app.use(express.static("public"));
|
|||||||
const server = http.createServer(app);
|
const server = http.createServer(app);
|
||||||
const wss = new WebSocket.Server({ server });
|
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_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: [] };
|
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 = {};
|
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: <your token>\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() {
|
function getRoomList() {
|
||||||
return Object.keys(rooms).map(id => ({
|
return Object.keys(rooms).map(id => ({
|
||||||
id,
|
id,
|
||||||
players: rooms[id].players.length,
|
players: rooms[id].players.length,
|
||||||
turn: rooms[id].game.turn
|
isBotRoom: rooms[id].players.some(p => p.isBot)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
function broadcastRoomList() {
|
function handleMove(roomId, role, { from, to }) {
|
||||||
const list = JSON.stringify({ type: "room_list", rooms: getRoomList() });
|
const room = rooms[roomId];
|
||||||
wss.clients.forEach(client => {
|
if (!room || room.game.winner || room.game.turn !== role) return;
|
||||||
if (client.readyState === WebSocket.OPEN && !client.roomId) {
|
const game = room.game;
|
||||||
client.send(list);
|
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) => {
|
wss.on("connection", (ws, req) => {
|
||||||
const urlParams = new URLSearchParams(req.url.split('?')[1]);
|
const urlParams = new URLSearchParams(req.url.split('?')[1]);
|
||||||
let roomId = urlParams.get("room");
|
let roomId = urlParams.get("room");
|
||||||
@@ -52,83 +201,55 @@ wss.on("connection", (ws, req) => {
|
|||||||
ws.roomId = roomId;
|
ws.roomId = roomId;
|
||||||
|
|
||||||
let player = room.players.find(p => p.sessionId === sessionId);
|
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) {
|
||||||
if (player.timeout) clearTimeout(player.timeout);
|
// Reconnecting existing player
|
||||||
player.ws = ws;
|
player.ws = ws;
|
||||||
} else {
|
} else if (room.players.length < 2) {
|
||||||
const role = room.players.length === 0 ? room.nextFirstRole : (room.nextFirstRole === "sheep" ? "wolf" : "sheep");
|
// New player joining: logic to pick the opposite role
|
||||||
player = { ws, role, sessionId, timeout: null };
|
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);
|
room.players.push(player);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Inform the client of their assigned role
|
||||||
ws.send(JSON.stringify({ type: "init", role: player.role, roomId }));
|
ws.send(JSON.stringify({ type: "init", role: player.role, roomId }));
|
||||||
|
|
||||||
broadcastRoomList();
|
broadcastRoomList();
|
||||||
broadcastState(roomId);
|
broadcastState(roomId);
|
||||||
|
|
||||||
|
// If a bot is involved, update its state
|
||||||
|
if (room.players.some(p => p.isBot)) sendBotState(roomId);
|
||||||
|
|
||||||
ws.on("message", msg => {
|
ws.on("message", msg => {
|
||||||
let data = JSON.parse(msg);
|
let data = JSON.parse(msg);
|
||||||
if (data.type === "restart") {
|
if (data.type === "restart") {
|
||||||
room.players.forEach(p => p.role = (p.role === "wolf" ? "sheep" : "wolf"));
|
swapRoles(room);
|
||||||
room.nextFirstRole = room.nextFirstRole === "sheep" ? "wolf" : "sheep";
|
|
||||||
room.game = { wolf: 5, sheep: [0, 1, 3], turn: "sheep", moveCount: 0, winner: null, lastMove: null };
|
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", () => {
|
// Critical: Re-sync roles for all human clients on restart
|
||||||
if (player) {
|
room.players.forEach(p => {
|
||||||
player.timeout = setTimeout(() => {
|
if (p.ws && p.ws.readyState === WebSocket.OPEN) {
|
||||||
room.players = room.players.filter(p => p.sessionId !== sessionId);
|
p.ws.send(JSON.stringify({ type: "init", role: p.role, roomId }));
|
||||||
if (room.players.length === 0) {
|
|
||||||
delete rooms[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) {
|
server.listen(3030, () => console.log("Web Server running on 3030"));
|
||||||
const room = rooms[roomId];
|
botServer.listen(3031, () => console.log("Bot TCP Server running on 3031"));
|
||||||
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"));
|
|
||||||
2
start.sh
2
start.sh
@@ -1,4 +1,4 @@
|
|||||||
docker stop wolf-sheep-container
|
docker stop wolf-sheep-container
|
||||||
docker rm wolf-sheep-container
|
docker rm wolf-sheep-container
|
||||||
docker build -t wolf-sheep .
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user