initial im
This commit is contained in:
commit
51f0415617
23 changed files with 1512 additions and 0 deletions
3
client-hodejs/.gitignore
vendored
Normal file
3
client-hodejs/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
package-lock.json
|
||||
*.js
|
13
client-hodejs/esbuild.mjs
Normal file
13
client-hodejs/esbuild.mjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import * as esbuild from 'esbuild'
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: ['rabbit.ts', 'targetpractice.ts', 'simplehunter.ts'],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: ['node22'],
|
||||
packages: 'external',
|
||||
outdir: './'
|
||||
})
|
21
client-hodejs/package.json
Normal file
21
client-hodejs/package.json
Normal file
|
@ -0,0 +1,21 @@
|
|||
{
|
||||
"name": "robotank-base",
|
||||
"version": "0.0.1",
|
||||
"description": "base robot functions for the robotank simulation",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "node esbuild.mjs"
|
||||
},
|
||||
"author": "",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"dependencies": {
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0"
|
||||
}
|
||||
}
|
134
client-hodejs/rabbit.ts
Normal file
134
client-hodejs/rabbit.ts
Normal file
|
@ -0,0 +1,134 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import { RobotStatus } from './robotstatus';
|
||||
import { RoboBase } from './robobase';
|
||||
|
||||
class Rabbit extends RoboBase {
|
||||
carrotX: number = 0;
|
||||
carrotY: number = 0;
|
||||
running = false;
|
||||
mode = 'MUNCH';
|
||||
munchtime = 0;
|
||||
ready() {
|
||||
console.log('rabbit is ready');
|
||||
}
|
||||
|
||||
start() {
|
||||
console.log('simulation started');
|
||||
this.cmd.powerRight = 0;
|
||||
this.cmd.powerLeft = 0;
|
||||
this.cmd.radarMin = 270;
|
||||
this.cmd.radarMax = 90;
|
||||
this.sendCmd();
|
||||
this.running = true;
|
||||
}
|
||||
|
||||
statusReady(status: RobotStatus) {
|
||||
if (!this.running) {
|
||||
return;
|
||||
}
|
||||
switch (this.mode) {
|
||||
case 'RUN':
|
||||
const dx = this.carrotX - status.posX;
|
||||
const dy = this.carrotY - status.posY;
|
||||
const dir = Math.atan2(dx, -dy) * 180 / Math.PI;
|
||||
const dst = Math.sqrt(dx * dx + dy * dy);
|
||||
let rot = dir - status.orientation;
|
||||
if (rot < -180) {
|
||||
rot += 360;
|
||||
}
|
||||
if (rot > 180) {
|
||||
rot -= 360;
|
||||
}
|
||||
rot = Math.min(Math.max(-90, rot), 90);
|
||||
let desiredSpeed = 1;
|
||||
if (dst < 30) {
|
||||
desiredSpeed = dst / 40;
|
||||
}
|
||||
const speedDiff = Math.sin(rot * Math.PI / 180.0);
|
||||
// desired speed of left chain
|
||||
let dsl = desiredSpeed + speedDiff;
|
||||
// desired speed of right chain
|
||||
let dsr = desiredSpeed - speedDiff;
|
||||
// prevent overflows, reduce to limits but keep relative difference if possible
|
||||
if (dsl > 1) {
|
||||
dsr -= Math.max(dsl - 1, -1);
|
||||
dsl = 1;
|
||||
} else if (dsr > 1) {
|
||||
dsl -= Math.max(dsr - 1, -1);
|
||||
dsr = 1;
|
||||
}
|
||||
this.cmd.powerLeft = dsl;
|
||||
this.cmd.powerRight = dsr;
|
||||
// close and slow enough?
|
||||
if (dst < 5 && Math.abs(status.chainSpeedLeft) < .1 && Math.abs(status.chainSpeedRight) < .1) {
|
||||
this.mode = 'MUNCH';
|
||||
this.cmd.powerLeft = 0;
|
||||
this.cmd.powerRight = 0;
|
||||
this.munchtime = 5 * 20; // 5 sec
|
||||
console.log("munch");
|
||||
}
|
||||
break;
|
||||
case "MUNCH":
|
||||
if (this.munchtime) {
|
||||
--this.munchtime;
|
||||
} else {
|
||||
this.mode = 'RUN';
|
||||
this.carrotX = Math.random() * (this.width - 20) + 10;
|
||||
this.carrotY = Math.random() * (this.height - 20) + 10;
|
||||
console.log("target reached, new target", this.carrotX, this.carrotY);
|
||||
}
|
||||
break;
|
||||
}
|
||||
if (status.contactPoints && status.contactPoints.length > 0) {
|
||||
status.contactPoints.forEach((cp) => {
|
||||
// adjust angle by radar rotation
|
||||
const angle = cp.angle + status.radarPos;
|
||||
// global angle from position
|
||||
const globalAngle = angle + status.orientation;
|
||||
const targetX = Math.sin(globalAngle * Math.PI / 180)* cp.dist + status.posX;
|
||||
const targetY = -Math.cos(globalAngle * Math.PI / 180)* cp.dist + status.posY;
|
||||
// run the other way +- 45 degree
|
||||
let newTargetAngle = globalAngle + 180 + Math.random()*90 - 45
|
||||
if (newTargetAngle >= 360) {
|
||||
newTargetAngle -= 360;
|
||||
}
|
||||
if (newTargetAngle < 0) {
|
||||
newTargetAngle += 360;
|
||||
}
|
||||
// target is 50-100 units away
|
||||
const newTargetDistance = Math.random() * 50 + 50;
|
||||
this.carrotX = status.posX + Math.sin(newTargetAngle * Math.PI / 180) * newTargetDistance;
|
||||
this.carrotY = status.posY - Math.cos(newTargetAngle * Math.PI / 180) * newTargetDistance;
|
||||
if (this.carrotX > this.width - 10) {
|
||||
this.carrotX = this.width - 10;
|
||||
}
|
||||
if (this.carrotY > this.height - 10) {
|
||||
this.carrotY = this.height - 10;
|
||||
}
|
||||
if (this.carrotX < 0) {
|
||||
this.carrotX = 10;
|
||||
}
|
||||
if (this.carrotY < 0) {
|
||||
this.carrotY = 10;
|
||||
}
|
||||
console.log("enemy at", targetX, targetY, "run to", this.carrotX, this.carrotY);
|
||||
});
|
||||
}
|
||||
this.sendCmd();
|
||||
}
|
||||
|
||||
lost() {
|
||||
console.log('we lost');
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
won() {
|
||||
console.log('we won');
|
||||
this.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
var rabbit = new Rabbit('rabbit', 'scout');
|
||||
rabbit.connect();
|
120
client-hodejs/robobase.ts
Normal file
120
client-hodejs/robobase.ts
Normal file
|
@ -0,0 +1,120 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import WebSocket from 'ws';
|
||||
import { RobotCmd } from './robotcmd';
|
||||
import { RobotStatus } from './robotstatus';
|
||||
|
||||
abstract class RoboBase {
|
||||
private pingTimeout: NodeJS.Timeout;
|
||||
private socket: WebSocket;
|
||||
private name: string;
|
||||
private model: string;
|
||||
protected cmd: RobotCmd;
|
||||
protected width: number;
|
||||
protected height: number;
|
||||
private lastUpdate: number;
|
||||
protected deltaTime: number;
|
||||
|
||||
constructor(name: string, model:string) {
|
||||
this.name = name;
|
||||
this.model = model;
|
||||
this.cmd = new RobotCmd();
|
||||
this.width = 0;
|
||||
this.height = 0;
|
||||
this.lastUpdate = 0;
|
||||
this.deltaTime = 0;
|
||||
}
|
||||
|
||||
connect() {
|
||||
this.socket = new WebSocket('ws://127.0.0.1:3000/robot', undefined, {
|
||||
perMessageDeflate: false,
|
||||
autoPong: true
|
||||
});
|
||||
this.socket.addEventListener('error', this.error);
|
||||
this.socket.addEventListener('open', () => {
|
||||
this.open();
|
||||
this.heartbeat();
|
||||
});
|
||||
this.socket.addEventListener('ping', this.heartbeat);
|
||||
this.socket.addEventListener('message', this.message);
|
||||
this.socket.addEventListener('close', () => {
|
||||
clearTimeout(this.pingTimeout);
|
||||
});
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
this.socket.close();
|
||||
}
|
||||
|
||||
error(...param) {
|
||||
console.error(param);
|
||||
}
|
||||
|
||||
private heartbeat = () => {
|
||||
clearTimeout(this.pingTimeout);
|
||||
this.pingTimeout = setTimeout(() => {
|
||||
this.socket.terminate();
|
||||
}, 30000 + 1000);
|
||||
}
|
||||
|
||||
message = (event: MessageEvent) => {
|
||||
this.heartbeat();
|
||||
const msgtext = event.data.toString();
|
||||
const tokens = msgtext.split(' ');
|
||||
if (tokens.length == 1) {
|
||||
switch (tokens[0]) {
|
||||
case 'lost':
|
||||
this.lost();
|
||||
break;
|
||||
case 'ready':
|
||||
this.ready();
|
||||
break;
|
||||
case 'start':
|
||||
this.start();
|
||||
break;
|
||||
case 'won':
|
||||
this.won();
|
||||
break;
|
||||
default:
|
||||
console.log('msg', tokens[0])
|
||||
}
|
||||
} else if (tokens.length == 2) {
|
||||
switch (tokens[0]) {
|
||||
case 'specs':
|
||||
const specs = JSON.parse(tokens[1]);
|
||||
this.width = specs['width'];
|
||||
this.height = specs['height'];
|
||||
break;
|
||||
case 'status':
|
||||
const status:RobotStatus = JSON.parse(tokens[1]);
|
||||
this.deltaTime = status.simulationTime - this.lastUpdate;
|
||||
this.lastUpdate = status.simulationTime;
|
||||
this.statusReady(status);
|
||||
break;
|
||||
default:
|
||||
console.log('unknown command tk2 ', msgtext);
|
||||
}
|
||||
} else {
|
||||
console.log('unknown command tk3', msgtext);
|
||||
}
|
||||
}
|
||||
|
||||
open() {
|
||||
this.socket.send('name ' + this.name);
|
||||
this.socket.send('model ' + this.model);
|
||||
}
|
||||
|
||||
sendCmd() {
|
||||
this.socket.send('cmd ' + JSON.stringify(this.cmd.toObject()));
|
||||
this.cmd.fire = false;
|
||||
}
|
||||
|
||||
abstract lost(): void;
|
||||
abstract ready(): void;
|
||||
abstract start(): void;
|
||||
abstract statusReady(status: RobotStatus): void;
|
||||
abstract won(): void;
|
||||
}
|
||||
|
||||
export { RoboBase }
|
1
client-hodejs/robotcmd.ts
Symbolic link
1
client-hodejs/robotcmd.ts
Symbolic link
|
@ -0,0 +1 @@
|
|||
../server-nodejs/robotcmd.ts
|
1
client-hodejs/robotstatus.ts
Symbolic link
1
client-hodejs/robotstatus.ts
Symbolic link
|
@ -0,0 +1 @@
|
|||
../server-nodejs/robotstatus.ts
|
132
client-hodejs/simplehunter.ts
Normal file
132
client-hodejs/simplehunter.ts
Normal file
|
@ -0,0 +1,132 @@
|
|||
import { RobotStatus } from "./robotstatus";
|
||||
import { RoboBase } from "./robobase";
|
||||
|
||||
class SimpleHunter extends RoboBase {
|
||||
targetX: number = 0;
|
||||
targetY: number = 0;
|
||||
fireCoolDown: number = 0;
|
||||
mode: string = "SCAN";
|
||||
ready() {
|
||||
console.log("simpleHunter is ready");
|
||||
this.cmd.radarMin = -1;
|
||||
this.cmd.radarMax = -1;
|
||||
this.sendCmd();
|
||||
}
|
||||
start() {
|
||||
console.log("simulation started");
|
||||
this.targetX = Math.random() * (this.width - 20) + 10;
|
||||
this.targetY = Math.random() * (this.height - 20) + 10;
|
||||
console.log("target", this.targetX, this.targetY);
|
||||
}
|
||||
driveToTarget(status: RobotStatus) {
|
||||
const dx = this.targetX - status.posX;
|
||||
const dy = this.targetY - status.posY;
|
||||
const dir = Math.atan2(dx, -dy) * 180 / Math.PI;
|
||||
const dst = Math.sqrt(dx * dx + dy * dy);
|
||||
let rot = dir - status.orientation;
|
||||
if (rot < -180) {
|
||||
rot += 360;
|
||||
}
|
||||
if (rot > 180) {
|
||||
rot -= 360;
|
||||
}
|
||||
rot = Math.min(Math.max(-90, rot), 90);
|
||||
let desiredSpeed = 1;
|
||||
if (this.mode === 'SCAN') {
|
||||
if (dst < 30) {
|
||||
desiredSpeed = dst / 40;
|
||||
}
|
||||
} else {
|
||||
if (dst < 75) {
|
||||
desiredSpeed = dst - 50 / 25;
|
||||
}
|
||||
}
|
||||
const speedDiff = Math.sin(rot * Math.PI / 180.0);
|
||||
// desired speed of left chain
|
||||
let dsl = desiredSpeed + speedDiff;
|
||||
// desired speed of right chain
|
||||
let dsr = desiredSpeed - speedDiff;
|
||||
// prevent overflows, reduce to limits but keep relative difference if possible
|
||||
if (dsl > 1) {
|
||||
dsr -= Math.max(dsl - 1, -1);
|
||||
dsl = 1;
|
||||
} else if (dsr > 1) {
|
||||
dsl -= Math.max(dsr - 1, -1);
|
||||
dsr = 1;
|
||||
}
|
||||
this.cmd.powerLeft = dsl;
|
||||
this.cmd.powerRight = dsr;
|
||||
// close and slow enough?
|
||||
if (dst < 5 && Math.abs(status.chainSpeedLeft) < .1 && Math.abs(status.chainSpeedRight) < .1 && this.mode === 'SCAN') {
|
||||
this.targetX = Math.random() * (this.width - 20) + 10;
|
||||
this.targetY = Math.random() * (this.height - 20) + 10;
|
||||
console.log("target reached, new target", this.targetX, this.targetY);
|
||||
}
|
||||
}
|
||||
|
||||
statusReady(status: RobotStatus) {
|
||||
this.driveToTarget(status);
|
||||
if (this.fireCoolDown > 0) {
|
||||
this.fireCoolDown -= this.deltaTime;
|
||||
}
|
||||
if (status.contactPoints && status.contactPoints.length > 0) {
|
||||
status.contactPoints.forEach((cp) => {
|
||||
// adjust angle by radar rotation
|
||||
const angle = (cp.angle + status.radarPos) % 360;
|
||||
// global angle from position
|
||||
let newTargetAngle = angle + status.orientation;
|
||||
if (newTargetAngle >= 360) {
|
||||
newTargetAngle -= 360;
|
||||
}
|
||||
if (newTargetAngle < 0) {
|
||||
newTargetAngle += 360;
|
||||
}
|
||||
this.targetX = Math.sin(newTargetAngle * Math.PI / 180)* (cp.dist - 50) + status.posX;
|
||||
this.targetY = -Math.cos(newTargetAngle * Math.PI / 180)* (cp.dist - 50) + status.posY;
|
||||
this.cmd.gunTarget = angle;
|
||||
console.log("enemy at", this.targetX, this.targetY, "dist", cp.dist, "angle", angle);
|
||||
this.mode = 'SEEK';
|
||||
this.cmd.radarMin = angle - 20;
|
||||
this.cmd.radarMax = angle + 20;
|
||||
// shoot if aimed close enough
|
||||
if (cp.dist < 100 && cp.dist > 25) {
|
||||
console.log("diff", angle, status.gunOrient, Math.abs((angle-status.gunOrient)%360));
|
||||
if (this.fireCoolDown <= 0 && (Math.abs(angle-status.gunOrient) < 2)) {
|
||||
this.cmd.fire = true;
|
||||
console.log("fire", cp.dist);
|
||||
this.fireCoolDown = 10;
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (this.mode === 'SEEK') { // lost track
|
||||
this.cmd.radarMin = (this.cmd.radarMin + 359.5) % 360;
|
||||
this.cmd.radarMax = (this.cmd.radarMax + .5) % 360;
|
||||
//console.log("seek radar", this.cmd.radarMin, this.cmd.radarMax);
|
||||
if (this.cmd.radarMax - this.cmd.radarMin > 180) {
|
||||
this.cmd.radarMin = -1;
|
||||
this.cmd.radarMax = -1;
|
||||
this.mode = 'SCAN';
|
||||
console.log("scan");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (status.causedDamage > 0) {
|
||||
console.log("we hit with damage", status.causedDamage);
|
||||
}
|
||||
this.sendCmd();
|
||||
}
|
||||
|
||||
lost() {
|
||||
console.log("we lost");
|
||||
this.disconnect();
|
||||
}
|
||||
won() {
|
||||
console.log("we won");
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var simpleHunter = new SimpleHunter("targetPractise", "scout");
|
||||
simpleHunter.connect();
|
39
client-hodejs/targetpractice.ts
Normal file
39
client-hodejs/targetpractice.ts
Normal file
|
@ -0,0 +1,39 @@
|
|||
import { RobotStatus } from "./robotstatus";
|
||||
import { RoboBase } from "./robobase";
|
||||
|
||||
class TargetPractice extends RoboBase {
|
||||
lastHealth = 100;
|
||||
ready() {
|
||||
console.log("targetPractice is ready");
|
||||
this.cmd.radarMin = -1;
|
||||
this.cmd.radarMax = -1;
|
||||
this.cmd.gunTarget = 0;
|
||||
this.sendCmd();
|
||||
}
|
||||
start() {
|
||||
console.log("simulation started");
|
||||
this.cmd.radarMin = -1;
|
||||
this.cmd.radarMax = -1;
|
||||
this.sendCmd();
|
||||
}
|
||||
statusReady(status: RobotStatus) {
|
||||
if (status.health != this.lastHealth) {
|
||||
console.log("got hit life left:", status.health);
|
||||
this.lastHealth = status.health;
|
||||
}
|
||||
// no actions, no send
|
||||
// this.sendCmd();
|
||||
}
|
||||
lost() {
|
||||
console.log("we lost");
|
||||
this.disconnect();
|
||||
}
|
||||
won() {
|
||||
console.log("we won, how?");
|
||||
this.disconnect();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
var targetPractice = new TargetPractice("targetPractice", "scout");
|
||||
targetPractice.connect();
|
3
server-nodejs/.gitignore
vendored
Normal file
3
server-nodejs/.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
node_modules
|
||||
*.js
|
||||
package-lock.json
|
13
server-nodejs/esbuild.mjs
Normal file
13
server-nodejs/esbuild.mjs
Normal file
|
@ -0,0 +1,13 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import * as esbuild from 'esbuild'
|
||||
|
||||
await esbuild.build({
|
||||
entryPoints: ['index.ts'],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: ['node10.4'],
|
||||
packages: 'external',
|
||||
outfile: 'index.js',
|
||||
})
|
186
server-nodejs/htdocs/index.html
Normal file
186
server-nodejs/htdocs/index.html
Normal file
|
@ -0,0 +1,186 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>WebSocket Chat</title>
|
||||
<style type="text/css">
|
||||
.hidden {
|
||||
display:none;
|
||||
}
|
||||
#log {
|
||||
display:inline-block; width:500px; height:500px; box-sizing: border-box;
|
||||
vertical-align:top;
|
||||
}
|
||||
#arena {
|
||||
display:inline-block; width: 1000px; height: 1000px; border:1px solid #000;
|
||||
position:relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
#input {
|
||||
display:block; width:600px; box-sizing: border-box;
|
||||
}
|
||||
#arena .robot {
|
||||
position:absolute;
|
||||
width: 8px;
|
||||
height: 20px;
|
||||
}
|
||||
#arena .robot .model {
|
||||
width: 8px;
|
||||
height: 20px;
|
||||
}
|
||||
#arena .robot .cannon {
|
||||
width:2px;
|
||||
left:3px;
|
||||
top: -31px;
|
||||
height:15px;
|
||||
background:#f00;
|
||||
display:block;
|
||||
position:relative;
|
||||
transform-origin: bottom center;
|
||||
}
|
||||
#arena .robot .radar {
|
||||
transform-origin: bottom left;
|
||||
left: 4px;
|
||||
top: -433px;
|
||||
position: relative;
|
||||
}
|
||||
#arena .bullet {
|
||||
position: absolute;
|
||||
background: #f00;
|
||||
width:4px;
|
||||
height:4px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="arena"></div>
|
||||
<textarea id="log" cols="30" rows="10"></textarea>
|
||||
<button id="clear" type="button">Clear Watch</button><br />
|
||||
|
||||
<script>
|
||||
window.robotLookup = {};
|
||||
function getRobot(uuid) {
|
||||
if (!window.robotLookup[uuid]) {
|
||||
const div = document.createElement("div");
|
||||
div.setAttribute("id", uuid);
|
||||
div.setAttribute("class", "robot");
|
||||
const img = document.createElement("img");
|
||||
img.setAttribute("src", "tank.png");
|
||||
img.setAttribute("class", "model");
|
||||
div.appendChild(img);
|
||||
const span = document.createElement("span");
|
||||
span.setAttribute("class", "cannon");
|
||||
div.appendChild(span);
|
||||
const radar = document.createElement("img");
|
||||
radar.setAttribute("src", "radar.png");
|
||||
radar.setAttribute("class", "radar");
|
||||
div.appendChild(radar);
|
||||
const arena = document.getElementById('arena');
|
||||
arena.appendChild(div);
|
||||
|
||||
window.robotLookup[uuid] = {
|
||||
"cannonElem": span,
|
||||
'imgElem': img,
|
||||
'modelElem': div,
|
||||
'radarElem': radar,
|
||||
'lastRadar': 0,
|
||||
};
|
||||
}
|
||||
return window.robotLookup[uuid];
|
||||
}
|
||||
const textarea = document.getElementById("log");
|
||||
const clear = document.getElementById('clear');
|
||||
const arena = document.getElementById('arena');
|
||||
const bullets = [];
|
||||
for(let i = 0; i < 10; ++i) {
|
||||
const bullet = document.createElement('div');
|
||||
bullet.setAttribute("class", "bullet hidden");
|
||||
bullets.push(bullet);
|
||||
arena.appendChild(bullet);
|
||||
}
|
||||
function openWebSocket() {
|
||||
const websocket = new WebSocket("ws://localhost:3000/monitor");
|
||||
websocket.onopen = function() {
|
||||
console.log("connection opened");
|
||||
}
|
||||
websocket.onclose = function() {
|
||||
console.log("connection closed");
|
||||
for (uuid in window.robotLookup) {
|
||||
window.robotLookup[uuid]['modelElem'].remove();
|
||||
};
|
||||
window.robotLookup = [];
|
||||
setTimeout(function(){
|
||||
openWebSocket();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
websocket.onmessage = handleMessage;
|
||||
}
|
||||
function handleMessage(e) {
|
||||
const tokens = e.data.split(" ");
|
||||
let robot;
|
||||
switch (tokens[0]) {
|
||||
case 'ready':
|
||||
case 'connected':
|
||||
break;
|
||||
case 'disconnected':
|
||||
robot = getRobot(tokens[1]);
|
||||
robot['modelElem'].remove();
|
||||
delete window.robotLookup[tokens[1]]
|
||||
break;
|
||||
case 'name':
|
||||
robot = getRobot(tokens[1]);
|
||||
robot["name"] = tokens[2];
|
||||
break;
|
||||
case 'model':
|
||||
robot = getRobot(tokens[1]);
|
||||
robot["model"] = tokens[2];
|
||||
break;
|
||||
case 'bullet':
|
||||
const activeBullets = JSON.parse(tokens[1]);
|
||||
for (let i = 0; i < 10; ++i) {
|
||||
if (i >= activeBullets.length) {
|
||||
bullets[i].classList.add("hidden");
|
||||
} else {
|
||||
bullets[i].classList.remove("hidden");
|
||||
bullets[i].style.left = (2*activeBullets[i][0]-2) + 'px';
|
||||
bullets[i].style.top = (2*activeBullets[i][1]-2) + 'px';
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'status':
|
||||
robot = getRobot(tokens[1]);
|
||||
const status = JSON.parse(tokens[2]);
|
||||
robot["status"] = status;
|
||||
const model = robot['modelElem'];
|
||||
model.style.left = (Math.floor(status["posX"]*2) - 5) + 'px';
|
||||
model.style.top = (Math.floor(status["posY"]*2) - 12.5) + 'px';
|
||||
model.style.transform = "rotate(" + Math.floor(status["orientation"])+ "deg)";
|
||||
const cannon = robot['cannonElem'];
|
||||
cannon.style.transform = "rotate(" + Math.floor(status["gunOrient"])+ "deg)";
|
||||
const radar = robot['radarElem'];
|
||||
let pos = Math.floor(status["radarPos"]);
|
||||
let diff = (pos - robot['lastRadar']);
|
||||
if (diff < -180) diff +=360;
|
||||
if (diff > 180) diff -=360;
|
||||
// pos = 0;
|
||||
//console.log(diff);
|
||||
if (diff < 0) { // swiping clock wise
|
||||
radar.style.transform = "rotate(" + pos + "deg)";
|
||||
} else {
|
||||
radar.style.transform = "rotate(" + pos + "deg) scaleX(-1)";
|
||||
}
|
||||
robot['lastRadar'] = Math.floor(status["radarPos"]);
|
||||
break;
|
||||
default:
|
||||
console.log("received message:", tokens);
|
||||
textarea.value += e.data+"\r\n";
|
||||
}
|
||||
}
|
||||
openWebSocket();
|
||||
clear.addEventListener('click', function() {
|
||||
textarea.value = '';
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
BIN
server-nodejs/htdocs/radar.png
Normal file
BIN
server-nodejs/htdocs/radar.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4 KiB |
BIN
server-nodejs/htdocs/tank.png
Normal file
BIN
server-nodejs/htdocs/tank.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 730 B |
17
server-nodejs/index.ts
Normal file
17
server-nodejs/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import { MyWebSocketServer } from './websockets/server';
|
||||
import { WorldSim } from './worldsim';
|
||||
|
||||
const worldSim = new WorldSim();
|
||||
const webserver = new MyWebSocketServer(worldSim);
|
||||
webserver.start();
|
||||
worldSim.start(2)
|
||||
.then( () => {
|
||||
webserver.stop().then( () => {
|
||||
//console.log("stopped", process._getActiveRequests());//,process._getActiveHandles(), );
|
||||
//console.log("stopped", process._getActiveHandles());
|
||||
process.exit(0);
|
||||
});
|
||||
});
|
19
server-nodejs/package.json
Normal file
19
server-nodejs/package.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"name": "server-nodejs",
|
||||
"version": "0.0.1",
|
||||
"description": "Server for robotank",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"build": "node esbuild.mjs"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"ws": "^8.18.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bufferutil": "^4.0.9"
|
||||
}
|
||||
}
|
182
server-nodejs/robot.ts
Normal file
182
server-nodejs/robot.ts
Normal file
|
@ -0,0 +1,182 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import { WorldSim } from './worldsim';
|
||||
import { RobotCmd } from './robotcmd';
|
||||
import { RobotStatusExt } from './robotstatus';
|
||||
|
||||
class Robot {
|
||||
private name: string;
|
||||
private model: string;
|
||||
private status: RobotStatusExt;
|
||||
private cmd: RobotCmd;
|
||||
private world: WorldSim;
|
||||
private uuid: string;
|
||||
private width: number;
|
||||
private height: number;
|
||||
private fireCooldown: number;
|
||||
private topSpeed: number = 20;
|
||||
private radarRange: number = 200;
|
||||
|
||||
constructor(worldsim: WorldSim, uuid: string, width: number, height: number) {
|
||||
this.world = worldsim;
|
||||
this.uuid = uuid;
|
||||
this.name = '';
|
||||
this.model = '';
|
||||
this.status = new RobotStatusExt();
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
this.status.posX = Math.random()*(width - 20) + 10,
|
||||
this.status.posY = Math.random()*(height - 20) + 10,
|
||||
this.status.orientation = Math.random()*360;
|
||||
this.fireCooldown = 0;
|
||||
this.cmd = new RobotCmd;
|
||||
this.cmd.mergeObject({
|
||||
powerLeft: 0,
|
||||
powerRight: 0,
|
||||
radarMin: 0,
|
||||
radarMax: 360,
|
||||
gunTarget: 0,
|
||||
fire: false
|
||||
});
|
||||
}
|
||||
|
||||
causedDamage(amount: number) {
|
||||
this.status.causedDamage = amount;
|
||||
}
|
||||
damage(amount: number) {
|
||||
this.status.health -= amount;
|
||||
}
|
||||
|
||||
getStatus() : RobotStatusExt {
|
||||
return this.status;
|
||||
}
|
||||
|
||||
getX() : number {
|
||||
return this.status.posX;
|
||||
}
|
||||
|
||||
getY() : number {
|
||||
return this.status.posY;
|
||||
}
|
||||
|
||||
isReady() : boolean {
|
||||
return this.name !== '' && this.model !== '';
|
||||
}
|
||||
|
||||
moveRobot(dt: number) {
|
||||
if (this.cmd.fire && this.fireCooldown <= 0) {
|
||||
this.world.fire(this.uuid, this.status.posX, this.status.posY, this.status.orientation + this.status.gunOrient);
|
||||
this.fireCooldown = 10; // 10 seconds to reload
|
||||
}
|
||||
if (this.fireCooldown > 0) {
|
||||
this.fireCooldown -= dt;
|
||||
}
|
||||
this.status.chainSpeedLeft += (0.3*(this.cmd.powerLeft-this.status.chainSpeedLeft));
|
||||
this.status.chainSpeedRight += (0.3*(this.cmd.powerRight-this.status.chainSpeedRight));
|
||||
const speed = (this.status.chainSpeedLeft + this.status.chainSpeedRight)/2.0;
|
||||
this.status.posX += speed * Math.sin(this.status.orientation * Math.PI / 180.0) * dt * this.topSpeed;
|
||||
this.status.posY -= speed * Math.cos(this.status.orientation * Math.PI / 180.0) * dt * this.topSpeed;
|
||||
if (this.status.posX < 0) {
|
||||
this.status.posX = 0;
|
||||
this.damage(1);
|
||||
}
|
||||
if (this.status.posY < 0) {
|
||||
this.status.posY = 0;
|
||||
this.damage(1);
|
||||
}
|
||||
if (this.status.posX > this.width) {
|
||||
this.status.posX = this.width;
|
||||
this.damage(1);
|
||||
}
|
||||
if (this.status.posY > this.height) {
|
||||
this.status.posY = this.height;
|
||||
this.damage(1);
|
||||
}
|
||||
|
||||
this.status.orientation += (this.status.chainSpeedLeft - this.status.chainSpeedRight) * 30 * dt;
|
||||
if (this.status.orientation >= 360.0) this.status.orientation -= 360.0;
|
||||
if (this.status.orientation < 0.0) this.status.orientation += 360.0;
|
||||
if (this.cmd.radarMax == this.cmd.radarMin && this.cmd.radarMax == -1) { // full 360
|
||||
this.status.radarPos += 60 * dt; // 60 degree per sec
|
||||
if (this.status.radarPos > 360) this.status.radarPos -= 360;
|
||||
} else if (this.status.radarInc) {
|
||||
this.status.radarPos = (this.status.radarPos + 60 * dt) % 360;
|
||||
if (Math.abs(this.cmd.radarMax - this.status.radarPos) < 30 && this.cmd.radarMax <= this.status.radarPos) {
|
||||
this.status.radarInc = false;
|
||||
}
|
||||
} else {
|
||||
this.status.radarPos = (360 + this.status.radarPos - 60 * dt) % 360;
|
||||
if (Math.abs(this.cmd.radarMin - this.status.radarPos)< 30 && this.cmd.radarMin < this.status.radarPos) {
|
||||
this.status.radarInc = true;
|
||||
}
|
||||
}
|
||||
if (Math.abs(this.status.gunOrient - this.cmd.gunTarget) > 0.1) {
|
||||
const relTarget = this.cmd.gunTarget;
|
||||
const relPos = this.status.gunOrient;
|
||||
const diffcw = ((relPos - relTarget)+360)%360;
|
||||
const diffccw = ((relTarget-relPos)+360)%360;
|
||||
//console.log(relTarget, relPos, diffcw, diffccw, diffcw > diffccw);
|
||||
const diff = Math.min(Math.abs((relTarget - relPos)%360), 2.5) * (diffcw > diffccw ? 1 : -1);
|
||||
this.status.gunOrient = (this.status.gunOrient + diff + 360) %360;
|
||||
}
|
||||
}
|
||||
|
||||
setName(name: string) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
setModel(model: string) {
|
||||
// TODO, read from file
|
||||
this.model = model;
|
||||
}
|
||||
setSimulationTime(time: number) {
|
||||
this.status.simulationTime = time;
|
||||
}
|
||||
|
||||
updateCmd(newCmd: RobotCmd) {
|
||||
this.cmd.mergeObject(newCmd);
|
||||
if (this.cmd.powerLeft > 1) {
|
||||
this.cmd.powerLeft = 1;
|
||||
}
|
||||
if (this.cmd.powerLeft < -1) {
|
||||
this.cmd.powerLeft = -1;
|
||||
}
|
||||
if (this.cmd.powerRight > 1) {
|
||||
this.cmd.powerRight = 1;
|
||||
}
|
||||
if (this.cmd.powerRight < -1) {
|
||||
this.cmd.powerRight = -1;
|
||||
}
|
||||
}
|
||||
|
||||
updateRadar() {
|
||||
this.status.contactPoints = [];
|
||||
this.world.robots.forEach((robot) => {
|
||||
if (robot != this) {
|
||||
// x distance to other robot
|
||||
const dx = robot.status.posX - this.status.posX;
|
||||
// y distance to other robot
|
||||
const dy = robot.status.posY - this.status.posY;
|
||||
// direction to other robot
|
||||
const dir = Math.atan2(dx, -dy) / Math.PI * 180;
|
||||
// distance to other robot
|
||||
const dst = Math.sqrt(dx * dx + dy * dy);
|
||||
// relative rotation to other robot
|
||||
let rot = dir - this.status.orientation - this.status.radarPos;
|
||||
// ensure -180 > rot < 180
|
||||
while (rot > 180) {
|
||||
rot -= 360;
|
||||
}
|
||||
while (rot < -180) {
|
||||
rot += 360;
|
||||
}
|
||||
if (((this.status.radarInc && rot>=-6 && rot <=0) || (!this.status.radarInc && rot>=0 && rot <=6)) && dst < this.radarRange) {
|
||||
this.status.contactPoints.push({angle: rot, dist: dst});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export { Robot };
|
51
server-nodejs/robotcmd.ts
Normal file
51
server-nodejs/robotcmd.ts
Normal file
|
@ -0,0 +1,51 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
class RobotCmd {
|
||||
powerLeft: number = 0;
|
||||
powerRight: number = 0;
|
||||
radarMin: number = -1;
|
||||
radarMax: number = -1;
|
||||
gunTarget: number = -1;
|
||||
fire?: boolean;
|
||||
|
||||
toObject() {
|
||||
const o: Object = {};
|
||||
if (this.powerLeft !== -9999) {
|
||||
o['powerLeft'] = this.powerLeft;
|
||||
}
|
||||
if (this.powerRight !== -9999) {
|
||||
o['powerRight'] = this.powerRight;
|
||||
}
|
||||
o['radarMin'] = this.radarMin;
|
||||
o['radarMax'] = this.radarMax;
|
||||
if (this.gunTarget !== -1) {
|
||||
o['gunTarget'] = this.gunTarget;
|
||||
}
|
||||
if (this.fire === true) {
|
||||
o['fire'] = true;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
mergeObject(o: Object) {
|
||||
if (o['powerLeft'] !== undefined) {
|
||||
this.powerLeft = o['powerLeft'];
|
||||
}
|
||||
if (o['powerRight'] !== undefined) {
|
||||
this.powerRight = o['powerRight'];
|
||||
}
|
||||
if (o['radarMin'] !== undefined) {
|
||||
this.radarMin = o['radarMin'];
|
||||
}
|
||||
if (o['radarMax'] !== undefined) {
|
||||
this.radarMax = o['radarMax'];
|
||||
}
|
||||
if (o['gunTarget'] !== undefined) {
|
||||
this.gunTarget = o['gunTarget'];
|
||||
}
|
||||
this.fire = o['fire'] === true;
|
||||
}
|
||||
}
|
||||
|
||||
export { RobotCmd }
|
83
server-nodejs/robotstatus.ts
Normal file
83
server-nodejs/robotstatus.ts
Normal file
|
@ -0,0 +1,83 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
interface ContactPoint {
|
||||
dist: number;
|
||||
angle: number;
|
||||
}
|
||||
class RobotStatus {
|
||||
/**
|
||||
* current health 0 = death, max depends on robot
|
||||
*/
|
||||
health: number;
|
||||
/**
|
||||
* x position, range 0..1000
|
||||
*/
|
||||
posX: number;
|
||||
/**
|
||||
* x position, range 0..1000
|
||||
*/
|
||||
posY: number;
|
||||
/**
|
||||
* rotation, range 0..360, 0: north, 90: east
|
||||
*/
|
||||
orientation: number;
|
||||
/**
|
||||
* gun orientation (relative to tank), range 0..360, 0 front, 90 right
|
||||
*/
|
||||
gunOrient: number
|
||||
/**
|
||||
* radar_position 0..360, 0 front, 90 right
|
||||
*/
|
||||
radarPos: number
|
||||
contactPoints: ContactPoint[];
|
||||
chainSpeedLeft: number;
|
||||
chainSpeedRight: number;
|
||||
simulationTime: number;
|
||||
causedDamage: number;
|
||||
|
||||
constructor() {
|
||||
this.health = 100;
|
||||
this.posX = 0;
|
||||
this.posY = 0;
|
||||
this.orientation = 0;
|
||||
this.gunOrient = 0;
|
||||
this.radarPos = 0;
|
||||
this.contactPoints = [];
|
||||
this.chainSpeedLeft = 0;
|
||||
this.chainSpeedRight = 0;
|
||||
this.simulationTime = 0;
|
||||
this.causedDamage = 0;
|
||||
}
|
||||
private prec(input: number): number {
|
||||
return Math.round(input * 100) / 100;
|
||||
}
|
||||
toJSON() : string {
|
||||
return JSON.stringify({
|
||||
health: this.health,
|
||||
posX: this.prec(this.posX),
|
||||
posY: this.prec(this.posY),
|
||||
orientation: this.prec(this.orientation),
|
||||
gunOrient: this.prec(this.gunOrient),
|
||||
radarPos: this.prec(this.radarPos),
|
||||
contactPoints: this.contactPoints,
|
||||
chainSpeedLeft: this.prec(this.chainSpeedLeft),
|
||||
chainSpeedRight: this.prec(this.chainSpeedRight),
|
||||
simulationTime: this.prec(this.simulationTime),
|
||||
causedDamage: this.prec(this.causedDamage),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class RobotStatusExt extends RobotStatus {
|
||||
radarInc: boolean;
|
||||
constructor() {
|
||||
super();
|
||||
this.radarInc = true;
|
||||
}
|
||||
toJSON() : string {
|
||||
return super.toJSON();
|
||||
}
|
||||
}
|
||||
|
||||
export {RobotStatus, RobotStatusExt};
|
42
server-nodejs/websockets/monitor.ts
Normal file
42
server-nodejs/websockets/monitor.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import { WebSocketServer } from "ws";
|
||||
|
||||
class MonitorWS extends WebSocketServer {
|
||||
constructor(options: any) {
|
||||
super(options);
|
||||
const that = this;
|
||||
this.on('connection', (ws: any) => {
|
||||
ws.on('error', this.errorHandler);
|
||||
ws.on('disconnect', this.errorHandler);
|
||||
ws.on('message', function (this: WebSocket, data: Buffer, isBinary: boolean ){
|
||||
that.message(this, data, isBinary);
|
||||
});
|
||||
// ...
|
||||
ws.send("Connected");
|
||||
});
|
||||
}
|
||||
|
||||
errorHandler = (a: any,b: any, c:any) => {
|
||||
console.log(a,b,c);
|
||||
}
|
||||
|
||||
broadcast = (msg: string) => {
|
||||
if (this.clients === undefined) {
|
||||
return;
|
||||
}
|
||||
this.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {;
|
||||
client.send(msg);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
message = (ws: WebSocket, data: Buffer, isBinary:boolean) => {
|
||||
//const message = data.toString();
|
||||
//console.log('received monitor: ', message);
|
||||
}
|
||||
}
|
||||
|
||||
export { MonitorWS }
|
106
server-nodejs/websockets/robot.ts
Normal file
106
server-nodejs/websockets/robot.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import { randomUUID, UUID } from 'crypto';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import { WorldSim } from '../worldsim';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
interface MyWebSocket extends WebSocket {
|
||||
isAlive: boolean;
|
||||
websocketid: string;
|
||||
heartbeat(): void;
|
||||
closeHandler(this: WebSocket): void;
|
||||
}
|
||||
|
||||
class RobotWS extends WebSocketServer {
|
||||
private idLookup: Map<String, WebSocket>;
|
||||
private worldSim: WorldSim;
|
||||
constructor(options: any, worldsim: WorldSim) {
|
||||
super(options);
|
||||
this.worldSim = worldsim;
|
||||
this.idLookup = new Map();
|
||||
const that = this;
|
||||
this.on('connection', function connection(ws: MyWebSocket) {
|
||||
ws.isAlive = true;
|
||||
ws.websocketid = randomUUID().toString();
|
||||
worldsim.register(ws.websocketid);
|
||||
that.idLookup.set(ws.websocketid, ws);
|
||||
ws.heartbeat = function() {
|
||||
this.isAlive = true;
|
||||
}
|
||||
ws.closeHandler = function(this: WebSocket) {
|
||||
that.closeHandler(<MyWebSocket>this);
|
||||
}
|
||||
ws.on('close', ws.closeHandler);
|
||||
ws.on('pong', ws.heartbeat);
|
||||
ws.on('message', function (this: WebSocket, data: Buffer, isBinary: boolean) {
|
||||
const t = <MyWebSocket>this;
|
||||
t.heartbeat();
|
||||
that.message(t, data, isBinary);
|
||||
});
|
||||
});
|
||||
const interval = setInterval(this.ping, 10000);
|
||||
this.on('close', function close() {
|
||||
//console.log("close", a);
|
||||
clearInterval(interval);
|
||||
});
|
||||
}
|
||||
|
||||
broadcast = (msg: string) => {
|
||||
if (this.clients === undefined) {
|
||||
return;
|
||||
}
|
||||
this.clients.forEach((client) => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
client.send(msg);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
closeHandler = (ws: MyWebSocket) => {
|
||||
this.worldSim.unregister(ws.websocketid);
|
||||
this.idLookup.delete(ws.websocketid);
|
||||
}
|
||||
|
||||
message = (ws: MyWebSocket, data: Buffer, isBinary:boolean) => {
|
||||
const message = data.toString();
|
||||
const uuid = ws.websocketid;
|
||||
const tokens = message.split(" ");
|
||||
if (tokens.length == 2) {
|
||||
switch(tokens[0]) {
|
||||
case 'cmd':
|
||||
this.worldSim.cmd(uuid, JSON.parse(tokens[1]));
|
||||
break;
|
||||
case 'model':
|
||||
this.worldSim.setModel(uuid, tokens[1])
|
||||
break;
|
||||
case 'name':
|
||||
this.worldSim.setName(uuid, tokens[1]);
|
||||
break;
|
||||
default:
|
||||
console.log("unknown command", message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ping = () => {
|
||||
if (this.clients === undefined) {
|
||||
return;
|
||||
}
|
||||
this.clients.forEach(function each(ws: WebSocket) {
|
||||
const mws = <MyWebSocket>ws;
|
||||
if (mws.isAlive === false) {
|
||||
return mws.terminate();
|
||||
}
|
||||
mws.isAlive = false;
|
||||
mws.ping();
|
||||
});
|
||||
}
|
||||
send(uuid: string, msg: string) {
|
||||
const ws = this.idLookup.get(uuid);
|
||||
ws?.send(msg);
|
||||
}
|
||||
}
|
||||
|
||||
export { RobotWS }
|
77
server-nodejs/websockets/server.ts
Normal file
77
server-nodejs/websockets/server.ts
Normal file
|
@ -0,0 +1,77 @@
|
|||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Sascha Nitsch https://contentnation.net/@grumpydevelop
|
||||
|
||||
import { createServer, Server } from 'http';
|
||||
import { MonitorWS } from './monitor';
|
||||
import { RobotWS } from './robot';
|
||||
import { WorldSim } from '../worldsim';
|
||||
import fs from 'fs';
|
||||
|
||||
class MyWebSocketServer {
|
||||
private server: Server;
|
||||
private monitor: MonitorWS;
|
||||
private robot: RobotWS;
|
||||
constructor(worldSim: WorldSim) {
|
||||
this.server = createServer();
|
||||
this.monitor = new MonitorWS({ noServer: true });
|
||||
this.robot = new RobotWS({ noServer: true }, worldSim);
|
||||
worldSim.setMonitors(this.monitor);
|
||||
worldSim.setRobots(this.robot);
|
||||
}
|
||||
start() {
|
||||
this.server.on('request', (request, res) => {
|
||||
const { pathname } = new URL(request.url || '', 'ws://base.url');
|
||||
let filename = 'htdocs' + pathname;
|
||||
if (pathname.endsWith('/')) {
|
||||
filename += 'index.html';
|
||||
}
|
||||
try {
|
||||
const stream = fs.createReadStream(filename);
|
||||
stream.on("error", () => {
|
||||
res.statusCode = 404;
|
||||
res.end("Not found");
|
||||
console.log("404", filename);
|
||||
});
|
||||
stream.on("open", () => {
|
||||
if (filename.endsWith('.html')) {
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
} else if (filename.endsWith('.css')) {
|
||||
res.setHeader("Content-Type", "text/css");
|
||||
} else if (filename.endsWith('.js')) {
|
||||
res.setHeader("Content-Type", "text/javascript");
|
||||
}
|
||||
stream.pipe(res);
|
||||
});
|
||||
}
|
||||
catch (e) {
|
||||
request.statusCode = 404;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
this.server.on('upgrade', (request, socket, head) => {
|
||||
const { pathname } = new URL(request.url || '', 'ws://base.url');
|
||||
if (pathname === '/monitor') {
|
||||
this.monitor.handleUpgrade(request, socket, head, (ws) =>{
|
||||
this.monitor.emit('connection', ws, request);
|
||||
});
|
||||
} else if (pathname === '/robot') {
|
||||
this.robot.handleUpgrade(request, socket, head, (ws) => {
|
||||
this.robot.emit('connection', ws, request);
|
||||
});
|
||||
} else {
|
||||
console.log("nope");
|
||||
socket.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
this.server.listen(3000);
|
||||
}
|
||||
|
||||
async stop() {
|
||||
this.server.closeAllConnections();
|
||||
// await this.httpTerminator.terminate();
|
||||
}
|
||||
}
|
||||
|
||||
export { MyWebSocketServer }
|
269
server-nodejs/worldsim.ts
Normal file
269
server-nodejs/worldsim.ts
Normal file
|
@ -0,0 +1,269 @@
|
|||
// 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 }
|
Loading…
Reference in a new issue