tanks

Blow up enemy tanks using code
git clone https://git.woozle.org/neale/tanks.git

tanks / contrib
Neale Pickett  ·  2024-11-05

jstanks.js

  1"use strict";
  2/*
  3 * jstanks: A forf/tanks implementation in javascript, based on the C version.
  4 * Copyright (C) 2014 Alyssa Milburn
  5 *
  6 * This program is free software: you can redistribute it and/or modify
  7 * it under the terms of the GNU General Public License as published by
  8 * the Free Software Foundation, either version 3 of the License, or
  9 * (at your option) any later version.
 10 *
 11 * This program is distributed in the hope that it will be useful, but
 12 * WITHOUT ANY WARRANTY; without even the implied warranty of
 13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 14 * General Public License for more details.
 15 *
 16 * You should have received a copy of the GNU General Public License
 17 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 18 */
 19
 20/*
 21 * Disclaimer: I warned you all that I don't know javascript.
 22 *
 23 * TODO:
 24 *  - memory functions
 25 *  - peer at the arithmetic FIXMEs
 26 *  - overflow/underflow checks
 27 *  - do the substacks properly, not as a parse-time hack
 28 *  - type checking
 29 *    (one of those two should stop '{ dup 1 exch if } dup 1 exch if' from working)
 30 *
 31 *  - tests
 32 *  - catch/show exceptions
 33 *  - save/load state from cookie
 34 *  - stack visualisation
 35 *  - display live desired/current speed, turret angle, etc
 36 *  - show live sensor state in the Sensors box too?
 37 *  - scoreboard
 38 *  - apply simultaneous fire fixes and/or other upstream changes
 39 */
 40
 41var TAU = 2 * Math.PI;
 42
 43var mod = function(a, b) { return a % b; };
 44var sq = function(a) { return a * a; };
 45
 46var rad2deg = function(r) { return Math.floor(360*(r)/TAU); };
 47var deg2rad = function(r) { return (r*TAU)/360; };
 48
 49/* Some in-game constants */
 50var TANK_MAX_SENSORS = 10;
 51var TANK_RADIUS = 7.5;
 52var TANK_SENSOR_RANGE = 100;
 53var TANK_CANNON_RECHARGE = 20; /* Turns to recharge cannon */
 54var TANK_CANNON_RANGE = (TANK_SENSOR_RANGE / 2);
 55var TANK_MAX_ACCEL = 35;
 56var TANK_MAX_TURRET_ROT = (TAU/8);
 57var TANK_TOP_SPEED = 7;
 58var TANK_FRICTION = 0.75;
 59
 60/* (tank radius + tank radius)^2 */
 61var TANK_COLLISION_ADJ2 = ((TANK_RADIUS + TANK_RADIUS) * (TANK_RADIUS + TANK_RADIUS));
 62
 63/* (Sensor range + tank radius)^2
 64 * If the distance^2 to the center of a tank <= TANK_SENSOR_ADJ2,
 65 * that tank is within sensor range. */
 66var TANK_SENSOR_ADJ2 = ((TANK_SENSOR_RANGE + TANK_RADIUS) * (TANK_SENSOR_RANGE + TANK_RADIUS));
 67
 68var TANK_CANNON_ADJ2 = ((TANK_CANNON_RANGE + TANK_RADIUS) * (TANK_CANNON_RANGE + TANK_RADIUS));
 69
 70// initial game grid spacing
 71var SPACING = 150;
 72
 73var MEMORY_SIZE = 10;
 74
 75var Forf = function() {
 76    this.mem = new Object();
 77    this.builtins = new Object();
 78
 79    this.builtins["debug!"] = function(myforf) { document.getElementById('debug').innerHTML = myforf.popData(); };
 80    var unfunc = function(func) {
 81        return function(myforf) {
 82            var a = myforf.popData();
 83            myforf.datastack.push(~~func(a)); // truncate, FIXME
 84        };
 85    };
 86    var binfunc = function(func) {
 87        return function(myforf) {
 88            var a = myforf.popData();
 89            var b = myforf.popData();
 90            myforf.datastack.push(~~func(b,a)); // truncate?, FIXME
 91        };
 92    };
 93    this.builtins["~"] = unfunc(function(a) { return ~a; });
 94    this.builtins["!"] = unfunc(function(a) { return !a; });
 95    this.builtins["+"] = binfunc(function(a, b) { return a+b; });
 96    this.builtins["-"] = binfunc(function(a, b) { return a-b; });
 97    this.builtins["/"] = binfunc(function(a, b) {
 98        if (b === 0) { throw "division by zero"; }
 99        return a/b;
100    });
101    this.builtins["%"] = binfunc(function(a, b) {
102        if (b === 0) { throw "division by zero"; }
103        return mod(a,b);
104    });
105    this.builtins["*"] = binfunc(function(a, b) { return a*b; });
106    this.builtins["&"] = binfunc(function(a, b) { return a&b; });
107    this.builtins["|"] = binfunc(function(a, b) { return a|b; });
108    this.builtins["^"] = binfunc(function(a, b) { return a^b; });
109    this.builtins["<<"] = binfunc(function(a, b) { return a<<b; });
110    this.builtins[">>"] = binfunc(function(a, b) { return a>>b; });
111    this.builtins[">"] = binfunc(function(a, b) { return a>b; });
112    this.builtins[">="] = binfunc(function(a, b) { return a>=b; });
113    this.builtins["<"] = binfunc(function(a, b) { return a<b; });
114    this.builtins["<="] = binfunc(function(a, b) { return a<=b; });
115    this.builtins["="] = binfunc(function(a, b) { return a===b; });
116    this.builtins["<>"] = binfunc(function(a, b) { return a!==b; });
117    this.builtins["abs"] = unfunc(function(a) { return Math.abs(a); });
118    // FIXME: the three following functions can only manipulate numbers in cforf
119    this.builtins["dup"] = function(myforf) {
120        var val = myforf.popData();
121        myforf.datastack.push(val);
122        myforf.datastack.push(val);
123    };
124    this.builtins["pop"] = function(myforf) {
125        myforf.popData();
126    };
127    this.builtins["exch"] = function(myforf) {
128       var a = myforf.popData();
129       var b = myforf.popData();
130       myforf.datastack.push(a);
131       myforf.datastack.push(b);
132    };
133    this.builtins["if"] = function(myforf) {
134       var ifclause = myforf.popData();
135       var cond = myforf.popData();
136       if (cond) {
137            // TODO: make sure ifclause is a list
138            for (var i = 0; i < ifclause.length; i++) {
139                myforf.cmdstack.push(ifclause[i]);
140            }
141        }
142    };
143    this.builtins["ifelse"] = function(myforf) {
144        var elseclause = myforf.popData();
145        var ifclause = myforf.popData();
146        var cond = myforf.popData();
147        if (!cond) {
148            ifclause = elseclause;
149        }
150        // TODO: make sure ifclause is a list
151        for (var i = 0; i < ifclause.length; i++) {
152            myforf.cmdstack.push(ifclause[i]);
153        }
154    };
155    this.builtins["mset"] = function(myforf) {
156        var pos = myforf.popData();
157        var a = myforf.popData();
158        if (pos < 0 || pos >= MEMORY_SIZE) {
159            throw "invalid memory location";
160        }
161        myforf.mem[pos] = a;
162    };
163    this.builtins["mget"] = function(myforf) {
164        var pos = myforf.popData();
165        if (pos < 0 || pos >= MEMORY_SIZE) {
166            throw "invalid memory location";
167        }
168        myforf.datastack.push(myforf.mem[pos]);
169    };
170};
171
172Forf.prototype.popData = function() {
173    if (this.datastack.length === 0) {
174        throw "tried to pop from empty stack";
175    }
176    return this.datastack.pop();
177};
178
179Forf.prototype.init = function(code) {
180    this.code = code;
181};
182
183Forf.prototype.parse = function() {
184    this.cmdstack = [];
185
186    // 'parse' the input
187    this.code = this.code.replace(/\([^)]*\)/g, "");
188    var splitCode = this.code.split(/([{}])/).join(" ");
189    var tokens = splitCode.split(/\s+/).filter(Boolean); // filter to deal with newlines etc
190    // FIXME: this is a hack right now because ugh stacks
191    var parseTokensAt = function(i, stack) {
192        var val = tokens[i];
193        if (val === "{") {
194            var dststack = [];
195            i = i + 1;
196            while (i < tokens.length) {
197                if (tokens[i] === "}") {
198                    break;
199                }
200                i = parseTokensAt(i, dststack);
201            }
202            stack.push(dststack.reverse());
203        } else {
204            // replace numbers with actual numbers
205            var n = parseInt(val);
206            if (String(n) === val) {
207                stack.push(n);
208            } else {
209                stack.push(val);
210            }
211        }
212        return i + 1;
213    };
214    var i = 0;
215    while (i < tokens.length) {
216        i = parseTokensAt(i, this.cmdstack);
217    }
218
219    // The first thing we read should be the first thing we do.
220    this.cmdstack = this.cmdstack.reverse();
221};
222
223Forf.prototype.run = function() {
224    this.datastack = [];
225
226    var running = true;
227    while (running && this.cmdstack.length) {
228        var val = this.cmdstack.pop();
229        if (typeof(val) == "string") {
230            var func = this.builtins[val];
231            if (val in this.builtins) {
232                func(this);
233            } else {
234                throw "no such function '" + val + "'";
235            }
236        } else {
237            this.datastack.push(val);
238        }
239    }
240};
241
242var gameSize = [0, 0];
243
244var ForfTank = function() {
245    // http://www.paulirish.com/2009/random-hex-color-code-snippets/
246    this.color = '#'+(4473924+Math.floor(Math.random()*12303291)).toString(16);
247
248    this.sensors = [];
249    this.position = [0, 0];
250    this.angle = 0;
251    this.speed = new Object;
252    this.speed.desired = [0, 0];
253    this.speed.current = [0, 0];
254    this.turret = new Object;
255    this.turret.current = 0;
256    this.turret.desired = 0;
257    this.turret.firing = 0;
258    this.turret.recharge = 0;
259    this.led = 0;
260    this.killer = null;
261    this.cause_death = "";
262
263    this.builtins["fire-ready?"] = function(myforf) {
264        myforf.datastack.push(myforf.fireReady());
265    };
266    this.builtins["fire!"] = function(myforf) {
267        myforf.fire();
268    };
269    this.builtins["set-speed!"] = function(myforf) {
270        var right = myforf.popData();
271        var left = myforf.popData();
272        myforf.setSpeed(left, right);
273    };
274    this.builtins["set-turret!"] = function(myforf) {
275        var angle = myforf.popData();
276        myforf.setTurret(deg2rad(angle));
277    };
278    this.builtins["get-turret"] = function(myforf) {
279        var angle = myforf.getTurret();
280        myforf.datastack.push(rad2deg(angle));
281    };
282    this.builtins["sensor?"] = function(myforf) {
283        var sensor_num = myforf.popData();
284        myforf.datastack.push(myforf.getSensor(sensor_num));
285    };
286    this.builtins["set-led!"] = function(myforf) {
287        var active = myforf.popData();
288        myforf.setLed(active);
289    };
290    this.builtins["random"] = function(myforf) {
291        var max = myforf.popData();
292        if (max < 1) {
293             myforf.datastack.push(0);
294             return;
295        }
296        myforf.datastack.push(Math.floor(Math.random() * max));
297    };
298};
299ForfTank.prototype = new Forf();
300ForfTank.prototype.constructor = ForfTank;
301
302ForfTank.prototype.addSensor = function(range, angle, width, turret) {
303    var sensor = new Object();
304    sensor.range = range;
305    sensor.angle = deg2rad(angle);
306    sensor.width = deg2rad(width);
307    sensor.turret = turret;
308    this.sensors.push(sensor);
309};
310
311ForfTank.prototype.fireReady = function() {
312    return !this.turret.recharge;
313};
314
315ForfTank.prototype.fire = function() {
316    this.turret.firing = this.fireReady();
317};
318
319ForfTank.prototype.setSpeed = function(left, right) {
320    this.speed.desired[0] = Math.min(Math.max(left, -100), 100);
321    this.speed.desired[1] = Math.min(Math.max(right, -100), 100);
322};
323
324ForfTank.prototype.getTurret = function() {
325    return this.turret.current;
326};
327
328ForfTank.prototype.setTurret = function(angle) {
329    this.turret.desired = mod(angle, TAU);
330};
331
332ForfTank.prototype.getSensor = function(sensor_num) {
333    if ((sensor_num < 0) || (sensor_num >= this.sensors.length)) {
334        return 0;
335    } else {
336        return this.sensors[sensor_num].triggered;
337    }
338};
339
340ForfTank.prototype.setLed = function(active) {
341    this.led = active;
342};
343
344ForfTank.prototype.move = function() {
345    var dir = 1;
346    var movement;
347    var angle;
348
349    /* Rotate the turret */
350    var rot_angle;              /* Quickest way there */
351
352    /* Constrain rot_angle to between -PI and PI */
353    rot_angle = this.turret.desired - this.turret.current;
354    while (rot_angle < 0) {
355        rot_angle += TAU;
356    }
357    rot_angle = mod(Math.PI + rot_angle, TAU) - Math.PI;
358
359    rot_angle = Math.min(TANK_MAX_TURRET_ROT, rot_angle);
360    rot_angle = Math.max(-TANK_MAX_TURRET_ROT, rot_angle);
361    this.turret.current = mod(this.turret.current + rot_angle, TAU);
362
363    /* Fakey acceleration */
364    for (var i = 0; i < 2; i++) {
365        if (this.speed.current[i] === this.speed.desired[i]) {
366            /* Do nothing */
367        } else if (this.speed.current[i] < this.speed.desired[i]) {
368            this.speed.current[i] = Math.min(this.speed.current[i] + TANK_MAX_ACCEL,
369                                             this.speed.desired[i]);
370        } else {
371            this.speed.current[i] = Math.max(this.speed.current[i] - TANK_MAX_ACCEL,
372                                             this.speed.desired[i]);
373        }
374    }
375
376    /* The simple case */
377    if (this.speed.current[0] === this.speed.current[1]) {
378        movement = this.speed.current[0] * (TANK_TOP_SPEED / 100.0);
379        angle = 0;
380    } else {
381        /* pflarr's original comment:
382         *
383         *   The tank drives around in a circle of radius r, which is some
384         *   offset on a line perpendicular to the tank.  The distance it
385         *   travels around the circle varies with the speed of each tread,
386         *   and is such that each side of the tank moves an equal angle
387         *   around the circle.
388         *
389         * Sounds good to me.   pflarr's calculations here are fantastico,
390         * there's nothing whatsoever to change. */
391
392        /* The first thing Paul's code does is find "friction", which seems
393           to be a penalty for having the treads go in opposite directions.
394           This probably plays hell with precisely-planned tanks, which I
395           find very ha ha. */
396        var friction = TANK_FRICTION * (Math.abs(this.speed.current[0] - this.speed.current[1]) / 200);
397        var v = [0, 0];
398        v[0] = this.speed.current[0] * (1 - friction) * (TANK_TOP_SPEED / 100.0);
399        v[1] = this.speed.current[1] * (1 - friction) * (TANK_TOP_SPEED / 100.0);
400
401        var Si;
402        var So;
403        /* Outside and inside speeds */
404        if (Math.abs(v[0]) > Math.abs(v[1])) {
405            Si = v[1];
406            So = v[0];
407            dir = 1;
408        } else {
409            Si = v[0];
410            So = v[1];
411            dir = -1;
412        }
413
414        /* Radius of circle to outside tread (use similar triangles) */
415        var r = So * (TANK_RADIUS * 2) / (So - Si);
416
417        /* pflarr:
418
419           The fraction of the circle traveled is equal to the speed
420           of the outer tread over the circumference of the circle:
421               Ft = So/(tau*r)
422           The angle traveled is:
423               theta = Ft * tau
424           This reduces to a simple
425               theta = So/r
426           We multiply it by dir to adjust for the direction of rotation
427        */
428        var theta = So/r * dir;
429
430        movement = r * Math.tan(theta);
431        angle = theta;
432    }
433
434    /* Now move the tank */
435    this.angle = mod(this.angle + angle + TAU, TAU);
436    var m = [0, 0];
437
438    m[0] = Math.cos(this.angle) * movement * dir;
439    m[1] = Math.sin(this.angle) * movement * dir;
440
441    for (var i = 0; i < 2; i++) {
442        this.position[i] = mod(this.position[i] + m[i] + gameSize[i], gameSize[i]);
443    }
444};
445
446var ftanks = [];
447var tanks = [];
448var interval = null;
449
450var initTanks = function(tanks) {
451    var ntanks = tanks.length;
452
453    // Calculate the size of the game board.
454    var x = 1;
455    while (x * x < ntanks) {
456        x = x + 1;
457    }
458    var y = Math.floor(ntanks / x);
459    if (ntanks % x) {
460        y = y + 1;
461    }
462    gameSize[0] = x * SPACING;
463    gameSize[1] = y * SPACING;
464
465    // Shuffle the order we place things on the game board.
466    var order = [];
467    for (var i = 0; i < ntanks; i++) {
468        order.push(i);
469    }
470    for (var i = 0; i < ntanks; i++) {
471        var j = Math.floor(Math.random() * ntanks);
472        var n = order[j];
473        order[j] = order[i];
474        order[i] = n;
475    }
476
477    // Position tanks.
478    x = SPACING / 2;
479    y = SPACING / 2;
480    for (var i = 0; i < ntanks; i++) {
481        tanks[order[i]].position[0] = x;
482        tanks[order[i]].position[1] = y;
483        // TODO: Move to constructor?
484        tanks[order[i]].angle = Math.random() * TAU;
485        tanks[order[i]].turret.current = Math.random() * TAU;
486        tanks[order[i]].turret.desired = tanks[order[i]].turret.current;
487
488        x = x + SPACING;
489        if (x > gameSize[0]) {
490            x = x % gameSize[0];
491            y = y + SPACING;
492        }
493    }
494};
495
496var rotate_point = function(angle, point) {
497    var cos_ = Math.cos(angle);
498    var sin_ = Math.sin(angle);
499
500    var newp = [0, 0];
501    newp[0] = point[0]*cos_ - point[1]*sin_;
502    newp[1] = point[0]*sin_ + point[1]*cos_;
503
504    point[0] = newp[0];
505    point[1] = newp[1];
506};
507
508ForfTank.prototype.fireCannon = function(that, vector, dist2) {
509    /* If someone's a crater, this is easy */
510    if ((this.killer && this.killer !== that) || that.killer) {
511        return;
512    }
513
514    /* Did they collide? */
515    if ((!this.killer) && dist2 < TANK_COLLISION_ADJ2) {
516        this.killer = that;
517        this.cause_death = "collision";
518
519        that.killer = this;
520        that.cause_death = "collision";
521
522        return;
523    }
524
525    /* No need to check if it's not even firing */
526    if (! this.turret.firing) {
527        return;
528    }
529
530    /* Also no need to check if it's outside cannon range */
531    if (dist2 > TANK_CANNON_ADJ2) {
532        return;
533    }
534
535    var theta = this.angle + this.turret.current;
536
537    /* Did this shoot that?  Rotate point by turret degrees, and if |y| <
538       TANK_RADIUS, we have a hit. */
539    var rpos = [vector[0], vector[1]];
540    rotate_point(-theta, rpos);
541    if ((rpos[0] > 0) && (Math.abs(rpos[1]) < TANK_RADIUS)) {
542        that.killer = this;
543        that.cause_death = "shot";
544    }
545};
546
547ForfTank.prototype.sensorCalc = function(that, vector, dist2) {
548    /* If someone's a crater, this is easy */
549    if (this.killer || that.killer) {
550        return;
551    }
552
553    /* If they're not inside the max sensor, just skip it */
554    if (dist2 > TANK_SENSOR_ADJ2) {
555        return;
556    }
557
558    /* Calculate sensors */
559    for (var i = 0; i < this.sensors.length; i++) {
560        if (0 === this.sensors[i].range) {
561            /* Sensor doesn't exist */
562            continue;
563        }
564
565        /* No need to re-check this sensor if it's already firing */
566        if (this.sensors[i].triggered) {
567            continue;
568        }
569
570        /* If the tank is out of range, don't bother */
571        if (dist2 > sq(this.sensors[i].range + TANK_RADIUS)) {
572            continue;
573        }
574
575        /* What is the angle of our sensor? */
576        var theta = this.angle + this.sensors[i].angle;
577        if (this.sensors[i].turret) {
578            theta += this.turret.current;
579        }
580
581        /* Rotate their position by theta */
582        var rpos = [vector[0], vector[1]];
583        rotate_point(-theta, rpos);
584
585        /* Sensor is symmetrical, we can consider only top quadrants */
586        rpos[1] = Math.abs(rpos[1]);
587
588        /* Compute inverse slopes to tank and of our sensor */
589        var m_s = 1 / Math.tan(this.sensors[i].width / 2);
590        var m_r = rpos[0] / rpos[1];
591
592        /* If our inverse slope is less than theirs, they're inside the arc */
593        if (m_r >= m_s) {
594            this.sensors[i].triggered = 1;
595            continue;
596        }
597
598        /* Now check if the edge of the arc intersects the tank.  Do this
599           just like with firing. */
600        rotate_point(this.sensors[i].width / -2, rpos);
601        if ((rpos[0] > 0) && (Math.abs(rpos[1]) < TANK_RADIUS)) {
602            this.sensors[i].triggered = 1;
603        }
604    }
605};
606
607var compute_vector = function(vector, _this, that) {
608    /* Establish shortest vector from center of this to center of that,
609     * taking wrapping into account */
610    for (var i = 0; i < 2; i += 1) {
611        var halfsize = gameSize[i] / 2;
612
613        vector[i] = that.position[i] - _this.position[i];
614        if (vector[i] > halfsize) {
615            vector[i] = vector[i] - gameSize[i];
616        } else if (vector[i] < -halfsize) {
617            vector[i] = gameSize[i] + vector[i];
618        }
619    }
620
621    /* Compute distance^2 for range comparisons */
622    return sq(vector[0]) + sq(vector[1]);
623};
624
625var updateTanks = function(tanks) {
626    /* Charge cannons and reset sensors */
627    for (var i = 0; i < tanks.length; i++) {
628        if (tanks[i].turret.firing) {
629            tanks[i].turret.firing = 0;
630            tanks[i].turret.recharge = TANK_CANNON_RECHARGE;
631        }
632        if (tanks[i].killer) {
633            continue;
634        }
635        if (tanks[i].turret.recharge) {
636            tanks[i].turret.recharge -= 1;
637        }
638        for (var j = 0; j < tanks[i].sensors.length; j += 1) {
639          tanks[i].sensors[j].triggered = 0;
640       }
641    }
642
643    /* Move tanks */
644    for (var i = 0; i < tanks.length; i++) {
645        if (tanks[i].killer) {
646            continue;
647        }
648        tanks[i].move();
649    }
650
651    /* Probe sensors */
652    for (var i = 0; i < tanks.length; i++) {
653        if (tanks[i].killer) {
654            continue;
655        }
656        for (var j = i + 1; j < tanks.length; j += 1) {
657            var _this = tanks[i];
658            var that = tanks[j];
659
660            var vector = [0, 0];
661            var dist2 = compute_vector(vector, _this, that);
662            _this.sensorCalc(that, vector, dist2);
663            vector[0] = -vector[0];
664            vector[1] = -vector[1];
665            that.sensorCalc(_this, vector, dist2);
666        }
667    }
668
669    /* Run programs */
670    var errors = [];
671    for (var i = 0; i < tanks.length; i++) {
672        if (tanks[i].killer) {
673            continue;
674        }
675        try {
676            tanks[i].parse(tanks[i].code);
677            tanks[i].run();
678        } catch (e) {
679            errors.push(e);
680        }
681    }
682    if (errors.length) {
683        if (interval) {
684            clearInterval(interval);
685        }
686
687        document.getElementById('debug').innerHTML = "Error: " + errors.join();
688        return;
689    }
690
691    /* Fire cannons and check for crashes */
692    for (var i = 0; i < tanks.length; i++) {
693        if (tanks[i].killer) {
694            continue;
695        }
696        for (var j = i + 1; j < tanks.length; j += 1) {
697            var _this = tanks[i];
698            var that = tanks[j];
699
700            var vector = [0, 0];
701            var dist2 = compute_vector(vector, _this, that);
702            _this.fireCannon(that, vector, dist2);
703            vector[0] = -vector[0];
704            vector[1] = -vector[1];
705            that.fireCannon(_this, vector, dist2);
706        }
707    }
708};
709
710var addBerzerker = function() {
711    var tank = new ForfTank();
712    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");
713    ftanks.push(tank);
714};
715
716var resetTanks = function() {
717    if (interval) {
718        clearInterval(interval);
719    }
720
721    document.getElementById('debug').innerHTML = "&nbsp;";
722
723    tanks = [];
724    ftanks = [];
725    var tank;
726
727    // add the user's tank
728    tank = new ForfTank();
729    tank.color = document.getElementsByName('color')[0].value;
730    for (var i = 0; i < 10; i++) {
731        var range = 1*document.getElementsByName('s'+i+'r')[0].value;
732        var angle = (1*document.getElementsByName('s'+i+'a')[0].value) % 360;
733        var width = (1*document.getElementsByName('s'+i+'w')[0].value) % 360;
734        var turret = 1*document.getElementsByName('s'+i+'t')[0].checked;
735        if (range) {
736            tank.addSensor(range, angle, width, turret);
737        }
738    }
739    var code = document.getElementById('program').value;
740    tank.init(code);
741    ftanks.push(tank);
742
743    var n = 6 + Math.floor(Math.random()*3);
744    for (var i = 0; i < n; i++) {
745        addBerzerker();
746    }
747
748    initTanks(ftanks);
749
750    var canvas = document.getElementById('battlefield');
751    canvas.width = gameSize[0];
752    canvas.height = gameSize[1];
753    var ctx = canvas.getContext('2d');
754    for (var i = 0; i < ftanks.length; i++) {
755        var sensors = [];
756        for (var j = 0; j < ftanks[i].sensors.length; j++) {
757            var s = ftanks[i].sensors[j];
758            var sensor = [s.range, s.angle, s.width, s.turret];
759            sensors.push(sensor);
760        }
761        tank = new Tank(ctx, canvas.width, canvas.height, ftanks[i].color, sensors);
762        tanks.push(tank);
763    }
764
765    function update() {
766        updateTanks(ftanks);
767
768        // clear
769        canvas.width = canvas.width;
770
771        var activeTanks = 0;
772        for (var i = 0; i < ftanks.length; i++) {
773            var flags = 0;
774            if (ftanks[i].turret.firing) {
775                flags |= 1;
776            }
777            if (ftanks[i].led) {
778                flags |= 2;
779            }
780            if (ftanks[i].killer) {
781                flags |= 4;
782            } else {
783                activeTanks++;
784            }
785            var sensor_state = 0;
786            for (var j = 0; j < ftanks[i].sensors.length; j++) {
787                if (ftanks[i].sensors[j].triggered) {
788                    sensor_state |= (1 << j);
789                }
790            }
791            tanks[i].set_state(ftanks[i].position[0], ftanks[i].position[1], ftanks[i].angle, ftanks[i].turret.current, flags, sensor_state);
792        }
793
794        if (activeTanks < 2) {
795            // we're done
796            clearInterval(interval);
797            interval = null;
798        }
799
800        for (var i = 0; i < ftanks.length; i++) {
801            tanks[i].draw_crater();
802        }
803
804        for (var i = 0; i < tanks.length; i++) {
805            tanks[i].draw_wrap_sensors();
806        }
807
808        for (var i = 0; i < tanks.length; i++) {
809            tanks[i].draw_tank();
810        }
811    }
812
813    interval = setInterval(update, 100 /*66*/);
814};
815