robotank/server-nodejs/worldsim.ts

269 lines
7.6 KiB
TypeScript
Raw Permalink Normal View History

2025-03-03 02:17:50 +01:00
// SPDX-License-Identifier: GPL-3.0-or-later
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
import { MonitorWS } from "./websockets/monitor";
import { RobotWS } from "./websockets/robot";
import { Robot } from "./robot";
import { RobotCmd } from "./robotcmd";
class Bullet {
private source: string;
public x: number;
public y: number;
private direction: number
private closest: number;
constructor(uuid: string, x: number, y: number, direction: number) {
this.source = uuid;
this.x = x;
this.y = y;
this.direction = direction * Math.PI / 180;
this.closest = 100;
}
advance(dt: number, robots: Map<String, Robot>, width: number, height: number) {
this.x += 100*Math.sin(this.direction) * dt;
this.y -= 100*Math.cos(this.direction) * dt;
if (this.x > width || this.x < 0 || this.y > height || this.y < 0) {
return true;
}
let damage = false;
robots.forEach((robot, uuid) => {
if (uuid === this.source) {
return;
}
const rX = robot.getX();
const rY = robot.getY();
if (Math.abs(rX - this.x) < 20 && Math.abs(rY - this.y) < 20) { // quick check
const dist = Math.sqrt((rX - this.x)*(rX - this.x) + (rY - this.y) * (rY - this.y));
//console.log("potential hit on", uuid, "from ",this.source, dist);
if (dist<10) {
if (dist > this.closest) {
const damageAmount = Math.pow(1.6, 10-this.closest)
robot.damage(damageAmount);
const sourceRobot = robots.get(this.source);
sourceRobot?.causedDamage(damageAmount);
damage = true;
} else {
this.closest = dist;
}
}
}
});
return damage;
}
}
class WorldSim {
private monitors: MonitorWS;
private simulationTime: number;
private worldTickTimer: NodeJS.Timeout;
private robotsWS: RobotWS;
public robots: Map<string, Robot>
private staged: Set<string>;
private ready: Set<string>
private numRobots: number;
private numRunsLeft: number;
private resolvePromise?: Function;
private winner: string;
private width: number;
private height: number;
private bullets: Array<Bullet>
constructor() {
this.simulationTime = 0;
this.robots = new Map();
this.staged = new Set();
this.ready = new Set();
this.numRobots = 0;
this.numRunsLeft = 99999;
this.winner = "";
this.width = 500;
this.height = 500;
this.bullets = [];
}
cmd(uuid: string, cmd: RobotCmd) {
const robot = this.robots.get(uuid);
if (robot) {
robot.updateCmd(cmd);
}
}
fire(uuid: string, posX, posY, direction) : void {
const bullet = new Bullet(uuid, posX, posY, direction);
this.bullets.push(bullet);
}
getTime() : number {
return this.simulationTime;
}
register(uuid: string) {
const robot = new Robot(this, uuid, this.width, this.height);
this.robots.set(uuid, robot);
this.monitors.broadcast("connected " + uuid);
this.staged.add(uuid);
}
robotReady(uuid: string) {
this.monitors.broadcast("ready " + uuid);
this.robotsWS.send(uuid, "ready");
this.staged.delete(uuid);
this.ready.add(uuid);
if (this.staged.size == 0 && this.ready.size == this.numRobots) {
--this.numRunsLeft;
this.monitors.broadcast("start");
this.ready.forEach((uuid: string) => {
this.robotsWS.send(uuid, "start");
});
this.worldTickTimer = setInterval(this.worldTick, 50);
}
}
setModel(uuid: string, model: string){
const robot = this.robots.get(uuid);
if (robot === undefined) {
return;
}
robot.setModel(model);
this.monitors.broadcast("model " + uuid + " " + model);
this.robotsWS.send(uuid, "specs " + JSON.stringify({width: this.width, height: this.height}));
const status = robot.getStatus();
const statusString = status.toJSON();
this.robotsWS.send(uuid, "status " + statusString);
this.monitors.broadcast('status ' + uuid + ' ' + statusString);
if(robot.isReady()) {
this.robotReady(uuid);
}
}
setMonitors(monitors: MonitorWS) {
this.monitors = monitors;
}
setName(uuid: string, name: string) {
//console.log("set name for ", uuid, "to", name);
const robot = this.robots.get(uuid);
if (robot === undefined) {
return;
}
robot.setName(name);
this.monitors.broadcast("name " + uuid + " " + name);
if(robot.isReady()) {
this.robotReady(uuid);
}
}
setRobots(robots: RobotWS) {
this.robotsWS = robots;
}
start(numRobots: number) : Promise<string> {
this.numRobots = numRobots;
return new Promise((resolve) => {
this.resolvePromise = resolve;
});
}
unregister(uuid: string) {
this.monitors.broadcast("disconnected " + uuid);
this.robots.delete(uuid);
this.ready.delete(uuid);
this.staged.delete(uuid);
if (this.numRunsLeft == 0 && this.staged.size == 0 && this.ready.size == 0) {
console.log("shut down");
if (this.resolvePromise) {
this.resolvePromise(this.winner);
}
}
}
lastTick = 0;
worldTick = () => {
const now = Date.now();
const dt = this.lastTick ? ((now - this.lastTick) / 1000) : 0.1;
this.lastTick = now;
this.simulationTime += dt;
this.ready.forEach((uuid: string) => {
const robot = this.robots.get(uuid);
robot?.setSimulationTime(this.simulationTime);
robot?.causedDamage(0);
});
// update bullets and check for hits
// run twice with half dt
for (let j = 0; j < 2; ++j) {
const toDelete:Array<number> = [];
const bulletPos: Array<Array<number>> = [];
this.bullets.forEach((bullet, i) => {
if (bullet.advance(dt / 2, this.robots, this.width, this.height)) {
toDelete.push(i);
} else {
bulletPos.push([bullet.x, bullet.y]);
}
});
if (toDelete.length) {
toDelete.reverse();
toDelete.forEach((index) => {
this.bullets.splice(index, 1);
});
}
if (this.bullets.length > 0 || toDelete.length > 0) {
this.monitors.broadcast('bullet ' + JSON.stringify(bulletPos));
}
}
// move robots according to "physics"
this.ready.forEach((uuid: string) => {
const robot = this.robots.get(uuid);
robot?.moveRobot(dt);
});
// update radar
this.ready.forEach((uuid: string) => {
const robot = this.robots.get(uuid);
robot?.updateRadar();
});
const killed: Array<string> = [];
this.ready.forEach((uuid: string) => {
const robot = this.robots.get(uuid);
if (robot !== undefined) {
const status = robot.getStatus();
if (status.health <= 0) {
// robot got killed
killed.push(uuid);
} else {
const statusString = status.toJSON();
/*if (status.contactPoints.length > 0) {
console.log(statusString);
}*/
this.robotsWS.send(uuid, "status " + statusString);
this.monitors.broadcast('status ' + uuid + ' ' + statusString);
}
}
});
killed.forEach((uuid) => {
this.monitors.broadcast("lost " + uuid);
this.robotsWS.send(uuid, "lost");
this.staged.add(uuid);
this.ready.delete(uuid);
});
if (this.ready.size == 1) {
this.ready.forEach((uuid) => {
this.monitors.broadcast("won " + uuid);
this.robotsWS.send(uuid, "won");
this.staged.add(uuid);
this.ready.delete(uuid);
this.winner = uuid;
});
clearInterval(this.worldTickTimer);
}
if (this.ready.size == 0) {
clearInterval(this.worldTickTimer);
}
}
}
export { WorldSim }