mirror of https://github.com/dirtbags/tanks.git
816 lines
25 KiB
JavaScript
816 lines
25 KiB
JavaScript
"use strict";
|
|
/*
|
|
* jstanks: A forf/tanks implementation in javascript, based on the C version.
|
|
* Copyright (C) 2014 Alyssa Milburn
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
/*
|
|
* Disclaimer: I warned you all that I don't know javascript.
|
|
*
|
|
* TODO:
|
|
* - memory functions
|
|
* - peer at the arithmetic FIXMEs
|
|
* - overflow/underflow checks
|
|
* - do the substacks properly, not as a parse-time hack
|
|
* - type checking
|
|
* (one of those two should stop '{ dup 1 exch if } dup 1 exch if' from working)
|
|
*
|
|
* - tests
|
|
* - catch/show exceptions
|
|
* - save/load state from cookie
|
|
* - stack visualisation
|
|
* - display live desired/current speed, turret angle, etc
|
|
* - show live sensor state in the Sensors box too?
|
|
* - scoreboard
|
|
* - apply simultaneous fire fixes and/or other upstream changes
|
|
*/
|
|
|
|
var TAU = 2 * Math.PI;
|
|
|
|
var mod = function(a, b) { return a % b; };
|
|
var sq = function(a) { return a * a; };
|
|
|
|
var rad2deg = function(r) { return Math.floor(360*(r)/TAU); };
|
|
var deg2rad = function(r) { return (r*TAU)/360; };
|
|
|
|
/* Some in-game constants */
|
|
var TANK_MAX_SENSORS = 10;
|
|
var TANK_RADIUS = 7.5;
|
|
var TANK_SENSOR_RANGE = 100;
|
|
var TANK_CANNON_RECHARGE = 20; /* Turns to recharge cannon */
|
|
var TANK_CANNON_RANGE = (TANK_SENSOR_RANGE / 2);
|
|
var TANK_MAX_ACCEL = 35;
|
|
var TANK_MAX_TURRET_ROT = (TAU/8);
|
|
var TANK_TOP_SPEED = 7;
|
|
var TANK_FRICTION = 0.75;
|
|
|
|
/* (tank radius + tank radius)^2 */
|
|
var TANK_COLLISION_ADJ2 = ((TANK_RADIUS + TANK_RADIUS) * (TANK_RADIUS + TANK_RADIUS));
|
|
|
|
/* (Sensor range + tank radius)^2
|
|
* If the distance^2 to the center of a tank <= TANK_SENSOR_ADJ2,
|
|
* that tank is within sensor range. */
|
|
var TANK_SENSOR_ADJ2 = ((TANK_SENSOR_RANGE + TANK_RADIUS) * (TANK_SENSOR_RANGE + TANK_RADIUS));
|
|
|
|
var TANK_CANNON_ADJ2 = ((TANK_CANNON_RANGE + TANK_RADIUS) * (TANK_CANNON_RANGE + TANK_RADIUS));
|
|
|
|
// initial game grid spacing
|
|
var SPACING = 150;
|
|
|
|
var MEMORY_SIZE = 10;
|
|
|
|
var Forf = function() {
|
|
this.mem = new Object();
|
|
this.builtins = new Object();
|
|
|
|
this.builtins["debug!"] = function(myforf) { document.getElementById('debug').innerHTML = myforf.popData(); };
|
|
var unfunc = function(func) {
|
|
return function(myforf) {
|
|
var a = myforf.popData();
|
|
myforf.datastack.push(~~func(a)); // truncate, FIXME
|
|
};
|
|
};
|
|
var binfunc = function(func) {
|
|
return function(myforf) {
|
|
var a = myforf.popData();
|
|
var b = myforf.popData();
|
|
myforf.datastack.push(~~func(b,a)); // truncate?, FIXME
|
|
};
|
|
};
|
|
this.builtins["~"] = unfunc(function(a) { return ~a; });
|
|
this.builtins["!"] = unfunc(function(a) { return !a; });
|
|
this.builtins["+"] = binfunc(function(a, b) { return a+b; });
|
|
this.builtins["-"] = binfunc(function(a, b) { return a-b; });
|
|
this.builtins["/"] = binfunc(function(a, b) {
|
|
if (b === 0) { throw "division by zero"; }
|
|
return a/b;
|
|
});
|
|
this.builtins["%"] = binfunc(function(a, b) {
|
|
if (b === 0) { throw "division by zero"; }
|
|
return mod(a,b);
|
|
});
|
|
this.builtins["*"] = binfunc(function(a, b) { return a*b; });
|
|
this.builtins["&"] = binfunc(function(a, b) { return a&b; });
|
|
this.builtins["|"] = binfunc(function(a, b) { return a|b; });
|
|
this.builtins["^"] = binfunc(function(a, b) { return a^b; });
|
|
this.builtins["<<"] = binfunc(function(a, b) { return a<<b; });
|
|
this.builtins[">>"] = binfunc(function(a, b) { return a>>b; });
|
|
this.builtins[">"] = binfunc(function(a, b) { return a>b; });
|
|
this.builtins[">="] = binfunc(function(a, b) { return a>=b; });
|
|
this.builtins["<"] = binfunc(function(a, b) { return a<b; });
|
|
this.builtins["<="] = binfunc(function(a, b) { return a<=b; });
|
|
this.builtins["="] = binfunc(function(a, b) { return a===b; });
|
|
this.builtins["<>"] = binfunc(function(a, b) { return a!==b; });
|
|
this.builtins["abs"] = unfunc(function(a) { return Math.abs(a); });
|
|
// FIXME: the three following functions can only manipulate numbers in cforf
|
|
this.builtins["dup"] = function(myforf) {
|
|
var val = myforf.popData();
|
|
myforf.datastack.push(val);
|
|
myforf.datastack.push(val);
|
|
};
|
|
this.builtins["pop"] = function(myforf) {
|
|
myforf.popData();
|
|
};
|
|
this.builtins["exch"] = function(myforf) {
|
|
var a = myforf.popData();
|
|
var b = myforf.popData();
|
|
myforf.datastack.push(a);
|
|
myforf.datastack.push(b);
|
|
};
|
|
this.builtins["if"] = function(myforf) {
|
|
var ifclause = myforf.popData();
|
|
var cond = myforf.popData();
|
|
if (cond) {
|
|
// TODO: make sure ifclause is a list
|
|
for (var i = 0; i < ifclause.length; i++) {
|
|
myforf.cmdstack.push(ifclause[i]);
|
|
}
|
|
}
|
|
};
|
|
this.builtins["ifelse"] = function(myforf) {
|
|
var elseclause = myforf.popData();
|
|
var ifclause = myforf.popData();
|
|
var cond = myforf.popData();
|
|
if (!cond) {
|
|
ifclause = elseclause;
|
|
}
|
|
// TODO: make sure ifclause is a list
|
|
for (var i = 0; i < ifclause.length; i++) {
|
|
myforf.cmdstack.push(ifclause[i]);
|
|
}
|
|
};
|
|
this.builtins["mset"] = function(myforf) {
|
|
var pos = myforf.popData();
|
|
var a = myforf.popData();
|
|
if (pos < 0 || pos >= MEMORY_SIZE) {
|
|
throw "invalid memory location";
|
|
}
|
|
myforf.mem[pos] = a;
|
|
};
|
|
this.builtins["mget"] = function(myforf) {
|
|
var pos = myforf.popData();
|
|
if (pos < 0 || pos >= MEMORY_SIZE) {
|
|
throw "invalid memory location";
|
|
}
|
|
myforf.datastack.push(myforf.mem[pos]);
|
|
};
|
|
};
|
|
|
|
Forf.prototype.popData = function() {
|
|
if (this.datastack.length === 0) {
|
|
throw "tried to pop from empty stack";
|
|
}
|
|
return this.datastack.pop();
|
|
};
|
|
|
|
Forf.prototype.init = function(code) {
|
|
this.code = code;
|
|
};
|
|
|
|
Forf.prototype.parse = function() {
|
|
this.cmdstack = [];
|
|
|
|
// 'parse' the input
|
|
this.code = this.code.replace(/\([^)]*\)/g, "");
|
|
var splitCode = this.code.split(/([{}])/).join(" ");
|
|
var tokens = splitCode.split(/\s+/).filter(Boolean); // filter to deal with newlines etc
|
|
// FIXME: this is a hack right now because ugh stacks
|
|
var parseTokensAt = function(i, stack) {
|
|
var val = tokens[i];
|
|
if (val === "{") {
|
|
var dststack = [];
|
|
i = i + 1;
|
|
while (i < tokens.length) {
|
|
if (tokens[i] === "}") {
|
|
break;
|
|
}
|
|
i = parseTokensAt(i, dststack);
|
|
}
|
|
stack.push(dststack.reverse());
|
|
} else {
|
|
// replace numbers with actual numbers
|
|
var n = parseInt(val);
|
|
if (String(n) === val) {
|
|
stack.push(n);
|
|
} else {
|
|
stack.push(val);
|
|
}
|
|
}
|
|
return i + 1;
|
|
};
|
|
var i = 0;
|
|
while (i < tokens.length) {
|
|
i = parseTokensAt(i, this.cmdstack);
|
|
}
|
|
|
|
// The first thing we read should be the first thing we do.
|
|
this.cmdstack = this.cmdstack.reverse();
|
|
};
|
|
|
|
Forf.prototype.run = function() {
|
|
this.datastack = [];
|
|
|
|
var running = true;
|
|
while (running && this.cmdstack.length) {
|
|
var val = this.cmdstack.pop();
|
|
if (typeof(val) == "string") {
|
|
var func = this.builtins[val];
|
|
if (val in this.builtins) {
|
|
func(this);
|
|
} else {
|
|
throw "no such function '" + val + "'";
|
|
}
|
|
} else {
|
|
this.datastack.push(val);
|
|
}
|
|
}
|
|
};
|
|
|
|
var gameSize = [0, 0];
|
|
|
|
var ForfTank = function() {
|
|
// http://www.paulirish.com/2009/random-hex-color-code-snippets/
|
|
this.color = '#'+(4473924+Math.floor(Math.random()*12303291)).toString(16);
|
|
|
|
this.sensors = [];
|
|
this.position = [0, 0];
|
|
this.angle = 0;
|
|
this.speed = new Object;
|
|
this.speed.desired = [0, 0];
|
|
this.speed.current = [0, 0];
|
|
this.turret = new Object;
|
|
this.turret.current = 0;
|
|
this.turret.desired = 0;
|
|
this.turret.firing = 0;
|
|
this.turret.recharge = 0;
|
|
this.led = 0;
|
|
this.killer = null;
|
|
this.cause_death = "";
|
|
|
|
this.builtins["fire-ready?"] = function(myforf) {
|
|
myforf.datastack.push(myforf.fireReady());
|
|
};
|
|
this.builtins["fire!"] = function(myforf) {
|
|
myforf.fire();
|
|
};
|
|
this.builtins["set-speed!"] = function(myforf) {
|
|
var right = myforf.popData();
|
|
var left = myforf.popData();
|
|
myforf.setSpeed(left, right);
|
|
};
|
|
this.builtins["set-turret!"] = function(myforf) {
|
|
var angle = myforf.popData();
|
|
myforf.setTurret(deg2rad(angle));
|
|
};
|
|
this.builtins["get-turret"] = function(myforf) {
|
|
var angle = myforf.getTurret();
|
|
myforf.datastack.push(rad2deg(angle));
|
|
};
|
|
this.builtins["sensor?"] = function(myforf) {
|
|
var sensor_num = myforf.popData();
|
|
myforf.datastack.push(myforf.getSensor(sensor_num));
|
|
};
|
|
this.builtins["set-led!"] = function(myforf) {
|
|
var active = myforf.popData();
|
|
myforf.setLed(active);
|
|
};
|
|
this.builtins["random"] = function(myforf) {
|
|
var max = myforf.popData();
|
|
if (max < 1) {
|
|
myforf.datastack.push(0);
|
|
return;
|
|
}
|
|
myforf.datastack.push(Math.floor(Math.random() * max));
|
|
};
|
|
};
|
|
ForfTank.prototype = new Forf();
|
|
ForfTank.prototype.constructor = ForfTank;
|
|
|
|
ForfTank.prototype.addSensor = function(range, angle, width, turret) {
|
|
var sensor = new Object();
|
|
sensor.range = range;
|
|
sensor.angle = deg2rad(angle);
|
|
sensor.width = deg2rad(width);
|
|
sensor.turret = turret;
|
|
this.sensors.push(sensor);
|
|
};
|
|
|
|
ForfTank.prototype.fireReady = function() {
|
|
return !this.turret.recharge;
|
|
};
|
|
|
|
ForfTank.prototype.fire = function() {
|
|
this.turret.firing = this.fireReady();
|
|
};
|
|
|
|
ForfTank.prototype.setSpeed = function(left, right) {
|
|
this.speed.desired[0] = Math.min(Math.max(left, -100), 100);
|
|
this.speed.desired[1] = Math.min(Math.max(right, -100), 100);
|
|
};
|
|
|
|
ForfTank.prototype.getTurret = function() {
|
|
return this.turret.current;
|
|
};
|
|
|
|
ForfTank.prototype.setTurret = function(angle) {
|
|
this.turret.desired = mod(angle, TAU);
|
|
};
|
|
|
|
ForfTank.prototype.getSensor = function(sensor_num) {
|
|
if ((sensor_num < 0) || (sensor_num >= this.sensors.length)) {
|
|
return 0;
|
|
} else {
|
|
return this.sensors[sensor_num].triggered;
|
|
}
|
|
};
|
|
|
|
ForfTank.prototype.setLed = function(active) {
|
|
this.led = active;
|
|
};
|
|
|
|
ForfTank.prototype.move = function() {
|
|
var dir = 1;
|
|
var movement;
|
|
var angle;
|
|
|
|
/* Rotate the turret */
|
|
var rot_angle; /* Quickest way there */
|
|
|
|
/* Constrain rot_angle to between -PI and PI */
|
|
rot_angle = this.turret.desired - this.turret.current;
|
|
while (rot_angle < 0) {
|
|
rot_angle += TAU;
|
|
}
|
|
rot_angle = mod(Math.PI + rot_angle, TAU) - Math.PI;
|
|
|
|
rot_angle = Math.min(TANK_MAX_TURRET_ROT, rot_angle);
|
|
rot_angle = Math.max(-TANK_MAX_TURRET_ROT, rot_angle);
|
|
this.turret.current = mod(this.turret.current + rot_angle, TAU);
|
|
|
|
/* Fakey acceleration */
|
|
for (var i = 0; i < 2; i++) {
|
|
if (this.speed.current[i] === this.speed.desired[i]) {
|
|
/* Do nothing */
|
|
} else if (this.speed.current[i] < this.speed.desired[i]) {
|
|
this.speed.current[i] = Math.min(this.speed.current[i] + TANK_MAX_ACCEL,
|
|
this.speed.desired[i]);
|
|
} else {
|
|
this.speed.current[i] = Math.max(this.speed.current[i] - TANK_MAX_ACCEL,
|
|
this.speed.desired[i]);
|
|
}
|
|
}
|
|
|
|
/* The simple case */
|
|
if (this.speed.current[0] === this.speed.current[1]) {
|
|
movement = this.speed.current[0] * (TANK_TOP_SPEED / 100.0);
|
|
angle = 0;
|
|
} else {
|
|
/* pflarr's original comment:
|
|
*
|
|
* The tank drives around in a circle of radius r, which is some
|
|
* offset on a line perpendicular to the tank. The distance it
|
|
* travels around the circle varies with the speed of each tread,
|
|
* and is such that each side of the tank moves an equal angle
|
|
* around the circle.
|
|
*
|
|
* Sounds good to me. pflarr's calculations here are fantastico,
|
|
* there's nothing whatsoever to change. */
|
|
|
|
/* The first thing Paul's code does is find "friction", which seems
|
|
to be a penalty for having the treads go in opposite directions.
|
|
This probably plays hell with precisely-planned tanks, which I
|
|
find very ha ha. */
|
|
var friction = TANK_FRICTION * (Math.abs(this.speed.current[0] - this.speed.current[1]) / 200);
|
|
var v = [0, 0];
|
|
v[0] = this.speed.current[0] * (1 - friction) * (TANK_TOP_SPEED / 100.0);
|
|
v[1] = this.speed.current[1] * (1 - friction) * (TANK_TOP_SPEED / 100.0);
|
|
|
|
var Si;
|
|
var So;
|
|
/* Outside and inside speeds */
|
|
if (Math.abs(v[0]) > Math.abs(v[1])) {
|
|
Si = v[1];
|
|
So = v[0];
|
|
dir = 1;
|
|
} else {
|
|
Si = v[0];
|
|
So = v[1];
|
|
dir = -1;
|
|
}
|
|
|
|
/* Radius of circle to outside tread (use similar triangles) */
|
|
var r = So * (TANK_RADIUS * 2) / (So - Si);
|
|
|
|
/* pflarr:
|
|
|
|
The fraction of the circle traveled is equal to the speed
|
|
of the outer tread over the circumference of the circle:
|
|
Ft = So/(tau*r)
|
|
The angle traveled is:
|
|
theta = Ft * tau
|
|
This reduces to a simple
|
|
theta = So/r
|
|
We multiply it by dir to adjust for the direction of rotation
|
|
*/
|
|
var theta = So/r * dir;
|
|
|
|
movement = r * Math.tan(theta);
|
|
angle = theta;
|
|
}
|
|
|
|
/* Now move the tank */
|
|
this.angle = mod(this.angle + angle + TAU, TAU);
|
|
var m = [0, 0];
|
|
|
|
m[0] = Math.cos(this.angle) * movement * dir;
|
|
m[1] = Math.sin(this.angle) * movement * dir;
|
|
|
|
for (var i = 0; i < 2; i++) {
|
|
this.position[i] = mod(this.position[i] + m[i] + gameSize[i], gameSize[i]);
|
|
}
|
|
};
|
|
|
|
var ftanks = [];
|
|
var tanks = [];
|
|
var interval = null;
|
|
|
|
var initTanks = function(tanks) {
|
|
var ntanks = tanks.length;
|
|
|
|
// Calculate the size of the game board.
|
|
var x = 1;
|
|
while (x * x < ntanks) {
|
|
x = x + 1;
|
|
}
|
|
var y = Math.floor(ntanks / x);
|
|
if (ntanks % x) {
|
|
y = y + 1;
|
|
}
|
|
gameSize[0] = x * SPACING;
|
|
gameSize[1] = y * SPACING;
|
|
|
|
// Shuffle the order we place things on the game board.
|
|
var order = [];
|
|
for (var i = 0; i < ntanks; i++) {
|
|
order.push(i);
|
|
}
|
|
for (var i = 0; i < ntanks; i++) {
|
|
var j = Math.floor(Math.random() * ntanks);
|
|
var n = order[j];
|
|
order[j] = order[i];
|
|
order[i] = n;
|
|
}
|
|
|
|
// Position tanks.
|
|
x = SPACING / 2;
|
|
y = SPACING / 2;
|
|
for (var i = 0; i < ntanks; i++) {
|
|
tanks[order[i]].position[0] = x;
|
|
tanks[order[i]].position[1] = y;
|
|
// TODO: Move to constructor?
|
|
tanks[order[i]].angle = Math.random() * TAU;
|
|
tanks[order[i]].turret.current = Math.random() * TAU;
|
|
tanks[order[i]].turret.desired = tanks[order[i]].turret.current;
|
|
|
|
x = x + SPACING;
|
|
if (x > gameSize[0]) {
|
|
x = x % gameSize[0];
|
|
y = y + SPACING;
|
|
}
|
|
}
|
|
};
|
|
|
|
var rotate_point = function(angle, point) {
|
|
var cos_ = Math.cos(angle);
|
|
var sin_ = Math.sin(angle);
|
|
|
|
var newp = [0, 0];
|
|
newp[0] = point[0]*cos_ - point[1]*sin_;
|
|
newp[1] = point[0]*sin_ + point[1]*cos_;
|
|
|
|
point[0] = newp[0];
|
|
point[1] = newp[1];
|
|
};
|
|
|
|
ForfTank.prototype.fireCannon = function(that, vector, dist2) {
|
|
/* If someone's a crater, this is easy */
|
|
if ((this.killer && this.killer !== that) || that.killer) {
|
|
return;
|
|
}
|
|
|
|
/* Did they collide? */
|
|
if ((!this.killer) && dist2 < TANK_COLLISION_ADJ2) {
|
|
this.killer = that;
|
|
this.cause_death = "collision";
|
|
|
|
that.killer = this;
|
|
that.cause_death = "collision";
|
|
|
|
return;
|
|
}
|
|
|
|
/* No need to check if it's not even firing */
|
|
if (! this.turret.firing) {
|
|
return;
|
|
}
|
|
|
|
/* Also no need to check if it's outside cannon range */
|
|
if (dist2 > TANK_CANNON_ADJ2) {
|
|
return;
|
|
}
|
|
|
|
var theta = this.angle + this.turret.current;
|
|
|
|
/* Did this shoot that? Rotate point by turret degrees, and if |y| <
|
|
TANK_RADIUS, we have a hit. */
|
|
var rpos = [vector[0], vector[1]];
|
|
rotate_point(-theta, rpos);
|
|
if ((rpos[0] > 0) && (Math.abs(rpos[1]) < TANK_RADIUS)) {
|
|
that.killer = this;
|
|
that.cause_death = "shot";
|
|
}
|
|
};
|
|
|
|
ForfTank.prototype.sensorCalc = function(that, vector, dist2) {
|
|
/* If someone's a crater, this is easy */
|
|
if (this.killer || that.killer) {
|
|
return;
|
|
}
|
|
|
|
/* If they're not inside the max sensor, just skip it */
|
|
if (dist2 > TANK_SENSOR_ADJ2) {
|
|
return;
|
|
}
|
|
|
|
/* Calculate sensors */
|
|
for (var i = 0; i < this.sensors.length; i++) {
|
|
if (0 === this.sensors[i].range) {
|
|
/* Sensor doesn't exist */
|
|
continue;
|
|
}
|
|
|
|
/* No need to re-check this sensor if it's already firing */
|
|
if (this.sensors[i].triggered) {
|
|
continue;
|
|
}
|
|
|
|
/* If the tank is out of range, don't bother */
|
|
if (dist2 > sq(this.sensors[i].range + TANK_RADIUS)) {
|
|
continue;
|
|
}
|
|
|
|
/* What is the angle of our sensor? */
|
|
var theta = this.angle + this.sensors[i].angle;
|
|
if (this.sensors[i].turret) {
|
|
theta += this.turret.current;
|
|
}
|
|
|
|
/* Rotate their position by theta */
|
|
var rpos = [vector[0], vector[1]];
|
|
rotate_point(-theta, rpos);
|
|
|
|
/* Sensor is symmetrical, we can consider only top quadrants */
|
|
rpos[1] = Math.abs(rpos[1]);
|
|
|
|
/* Compute inverse slopes to tank and of our sensor */
|
|
var m_s = 1 / Math.tan(this.sensors[i].width / 2);
|
|
var m_r = rpos[0] / rpos[1];
|
|
|
|
/* If our inverse slope is less than theirs, they're inside the arc */
|
|
if (m_r >= m_s) {
|
|
this.sensors[i].triggered = 1;
|
|
continue;
|
|
}
|
|
|
|
/* Now check if the edge of the arc intersects the tank. Do this
|
|
just like with firing. */
|
|
rotate_point(this.sensors[i].width / -2, rpos);
|
|
if ((rpos[0] > 0) && (Math.abs(rpos[1]) < TANK_RADIUS)) {
|
|
this.sensors[i].triggered = 1;
|
|
}
|
|
}
|
|
};
|
|
|
|
var compute_vector = function(vector, _this, that) {
|
|
/* Establish shortest vector from center of this to center of that,
|
|
* taking wrapping into account */
|
|
for (var i = 0; i < 2; i += 1) {
|
|
var halfsize = gameSize[i] / 2;
|
|
|
|
vector[i] = that.position[i] - _this.position[i];
|
|
if (vector[i] > halfsize) {
|
|
vector[i] = vector[i] - gameSize[i];
|
|
} else if (vector[i] < -halfsize) {
|
|
vector[i] = gameSize[i] + vector[i];
|
|
}
|
|
}
|
|
|
|
/* Compute distance^2 for range comparisons */
|
|
return sq(vector[0]) + sq(vector[1]);
|
|
};
|
|
|
|
var updateTanks = function(tanks) {
|
|
/* Charge cannons and reset sensors */
|
|
for (var i = 0; i < tanks.length; i++) {
|
|
if (tanks[i].turret.firing) {
|
|
tanks[i].turret.firing = 0;
|
|
tanks[i].turret.recharge = TANK_CANNON_RECHARGE;
|
|
}
|
|
if (tanks[i].killer) {
|
|
continue;
|
|
}
|
|
if (tanks[i].turret.recharge) {
|
|
tanks[i].turret.recharge -= 1;
|
|
}
|
|
for (var j = 0; j < tanks[i].sensors.length; j += 1) {
|
|
tanks[i].sensors[j].triggered = 0;
|
|
}
|
|
}
|
|
|
|
/* Move tanks */
|
|
for (var i = 0; i < tanks.length; i++) {
|
|
if (tanks[i].killer) {
|
|
continue;
|
|
}
|
|
tanks[i].move();
|
|
}
|
|
|
|
/* Probe sensors */
|
|
for (var i = 0; i < tanks.length; i++) {
|
|
if (tanks[i].killer) {
|
|
continue;
|
|
}
|
|
for (var j = i + 1; j < tanks.length; j += 1) {
|
|
var _this = tanks[i];
|
|
var that = tanks[j];
|
|
|
|
var vector = [0, 0];
|
|
var dist2 = compute_vector(vector, _this, that);
|
|
_this.sensorCalc(that, vector, dist2);
|
|
vector[0] = -vector[0];
|
|
vector[1] = -vector[1];
|
|
that.sensorCalc(_this, vector, dist2);
|
|
}
|
|
}
|
|
|
|
/* Run programs */
|
|
var errors = [];
|
|
for (var i = 0; i < tanks.length; i++) {
|
|
if (tanks[i].killer) {
|
|
continue;
|
|
}
|
|
try {
|
|
tanks[i].parse(tanks[i].code);
|
|
tanks[i].run();
|
|
} catch (e) {
|
|
errors.push(e);
|
|
}
|
|
}
|
|
if (errors.length) {
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
}
|
|
|
|
document.getElementById('debug').innerHTML = "Error: " + errors.join();
|
|
return;
|
|
}
|
|
|
|
/* Fire cannons and check for crashes */
|
|
for (var i = 0; i < tanks.length; i++) {
|
|
if (tanks[i].killer) {
|
|
continue;
|
|
}
|
|
for (var j = i + 1; j < tanks.length; j += 1) {
|
|
var _this = tanks[i];
|
|
var that = tanks[j];
|
|
|
|
var vector = [0, 0];
|
|
var dist2 = compute_vector(vector, _this, that);
|
|
_this.fireCannon(that, vector, dist2);
|
|
vector[0] = -vector[0];
|
|
vector[1] = -vector[1];
|
|
that.fireCannon(_this, vector, dist2);
|
|
}
|
|
}
|
|
};
|
|
|
|
var addBerzerker = function() {
|
|
var tank = new ForfTank();
|
|
tank.init("2 random 0 = { 50 100 set-speed! } { 100 50 set-speed! } ifelse 4 random 0 = { 360 random set-turret! } if 30 random 0 = { fire! } if");
|
|
ftanks.push(tank);
|
|
};
|
|
|
|
var resetTanks = function() {
|
|
if (interval) {
|
|
clearInterval(interval);
|
|
}
|
|
|
|
document.getElementById('debug').innerHTML = " ";
|
|
|
|
tanks = [];
|
|
ftanks = [];
|
|
var tank;
|
|
|
|
// add the user's tank
|
|
tank = new ForfTank();
|
|
tank.color = document.getElementsByName('color')[0].value;
|
|
for (var i = 0; i < 10; i++) {
|
|
var range = 1*document.getElementsByName('s'+i+'r')[0].value;
|
|
var angle = (1*document.getElementsByName('s'+i+'a')[0].value) % 360;
|
|
var width = (1*document.getElementsByName('s'+i+'w')[0].value) % 360;
|
|
var turret = 1*document.getElementsByName('s'+i+'t')[0].checked;
|
|
if (range) {
|
|
tank.addSensor(range, angle, width, turret);
|
|
}
|
|
}
|
|
var code = document.getElementById('program').value;
|
|
tank.init(code);
|
|
ftanks.push(tank);
|
|
|
|
var n = 6 + Math.floor(Math.random()*3);
|
|
for (var i = 0; i < n; i++) {
|
|
addBerzerker();
|
|
}
|
|
|
|
initTanks(ftanks);
|
|
|
|
var canvas = document.getElementById('battlefield');
|
|
canvas.width = gameSize[0];
|
|
canvas.height = gameSize[1];
|
|
var ctx = canvas.getContext('2d');
|
|
for (var i = 0; i < ftanks.length; i++) {
|
|
var sensors = [];
|
|
for (var j = 0; j < ftanks[i].sensors.length; j++) {
|
|
var s = ftanks[i].sensors[j];
|
|
var sensor = [s.range, s.angle, s.width, s.turret];
|
|
sensors.push(sensor);
|
|
}
|
|
tank = new Tank(ctx, canvas.width, canvas.height, ftanks[i].color, sensors);
|
|
tanks.push(tank);
|
|
}
|
|
|
|
function update() {
|
|
updateTanks(ftanks);
|
|
|
|
// clear
|
|
canvas.width = canvas.width;
|
|
|
|
var activeTanks = 0;
|
|
for (var i = 0; i < ftanks.length; i++) {
|
|
var flags = 0;
|
|
if (ftanks[i].turret.firing) {
|
|
flags |= 1;
|
|
}
|
|
if (ftanks[i].led) {
|
|
flags |= 2;
|
|
}
|
|
if (ftanks[i].killer) {
|
|
flags |= 4;
|
|
} else {
|
|
activeTanks++;
|
|
}
|
|
var sensor_state = 0;
|
|
for (var j = 0; j < ftanks[i].sensors.length; j++) {
|
|
if (ftanks[i].sensors[j].triggered) {
|
|
sensor_state |= (1 << j);
|
|
}
|
|
}
|
|
tanks[i].set_state(ftanks[i].position[0], ftanks[i].position[1], ftanks[i].angle, ftanks[i].turret.current, flags, sensor_state);
|
|
}
|
|
|
|
if (activeTanks < 2) {
|
|
// we're done
|
|
clearInterval(interval);
|
|
interval = null;
|
|
}
|
|
|
|
for (var i = 0; i < ftanks.length; i++) {
|
|
tanks[i].draw_crater();
|
|
}
|
|
|
|
for (var i = 0; i < tanks.length; i++) {
|
|
tanks[i].draw_wrap_sensors();
|
|
}
|
|
|
|
for (var i = 0; i < tanks.length; i++) {
|
|
tanks[i].draw_tank();
|
|
}
|
|
}
|
|
|
|
interval = setInterval(update, 100 /*66*/);
|
|
};
|
|
|