» Open raw file Spacewar_3_1.js.
» See the script in action.
/*
Spacewar! 3.1, JS port by Norbert Landsteiner <www.masswerk.at>, March 2015.
Updated for true implementation of the outline compiler in April 2016.
See: <http://www.masswerk.at/spacewar/JS-Spacewar/>.
Spacewar! was conceived in 1961 by Martin Graetz, Stephen Russell, and Wayne
Wiitanen. It was first realized on the PDP-1 in 1962 by Stephen Russell, Peter
Samson, Dan Edwards, and Martin Graetz, together with Alan Kotok, Steve Piner,
and Robert A Saunders. Spacewar! is in the public domain, but this credit
paragraph must accompany all distributed versions of the program.
The original program was written in PDP-1 assembler code, using MIT's "Macro"
assembler: <http://www.masswerk.at/spacewar/sources/spacewar3.1_complete.txt>
The JS implementation is intentionally close to the original, but makes use of
modern programming techniques, like loops, objects, and floating point math.
The script uses intentionally a subset of JavaScript that should be comprehen-
sible to anyone familiar with C or C-type language. (Tripple comparison opera-
tors like '===' and '!==' are checking identiy without type-coercion.)
The use of floating math has some effects on the implementation: multiplica-
tions and divisision will be used to transform values (rather than shifts),
also, some scalings in the original that are translating values to screen
co-ordinates by a shift by 8 bits will be affected, too. (The PDP-1 uses screen
co-ordinates in fixed point representation in one's complement, 10 significant
integer bits and 8 fractional bits, which were used for object-positions, too.)
The function generated by the outline compiler receives positional values and
unit vectors for its movement matrix rather as arguments than using shared
globals. Moreover, the JS implementation generally uses count-downs rather than
count-ups (the PDP-1 featured only an increment instruction but no decrement).
Otherwise the implementation is a direct port and true to the original code
(spacewar 3.1, 24 sep 62).
Compare the code analysis at <http://www.masswerk.at/spacewar/inside>
and the original program in emulation at <http://www.masswerk.at/spacewar>.
Requires an external display implementing
- CRT.plot(<x>, <y>, <brightness>) ... plot a blip onto the screen, and
- CRT.update() ... signal for final rendering at the end of each frame.
Optionally, an external UI receives the folling notifications:
- SpacewarUI.showScores(<score-ss1>, <score-ss2>) ... score display,
- SpacewarUI.halted() ... game halted (call Spacewar.resume() to continue),
- SpacewarUI.readGamepads() ... signal to check any controls on frame entry.
SpacewarUI is only called, if found, any of these methods may be missing.
The UI is expected to set player controls via Spacewar.setControls().
Options may be specified as an argument to run() or anytime via setOption().
==================== Not intended for commercial use. ====================
*/
var Spacewar = (function() {
"use strict";
// constants for triangular math (rotation)
var BIN_RAD_COEF = Math.PI / 51472, // PI is 51472 (0144420 oct) in PDP-1
TAU = Math.PI * 2; // 2 PI
// game settings
var Options = {
ANGULARMOMENTUM: false, // sense switch 1
LOWGRAVITY: false, // sense switch 2
SINGLESHOTS: false, // sense switch 3
NOBACKGROUND: false, // sense switch 4
SUNKILLS: false, // sense switch 5
SUNOFF: false, // sense switch 6
TESTWORDCONTROLS: false, // use testword controls
HALTONSCORES: false, // halt on scores/matches (call resume() to continue)
FPS: 22 // fps (original alternates between 19 and 25)
};
// "interesting and often changed constants"
// (original values in octal, not allowed in JS strict-mode, see comments)
// sym value unit/comment
// -------------------------------
var torpedoSupply = 32, // tno 040 (number of)
torpedoVelocity = 4, // tvl sar 4s (right shift)
torpedoReloadTime = 16, // rlt 020 (frames)
torpedoLife = 96, // tlf 0140 (frames)
fuelSupply = 8192, // foo 020000 (per exhaust blip)
angularAcceleration = 8 * BIN_RAD_COEF, // maa 010 (turn, PI: 0144420)
spaceshipAcceleration = 4, // sac sar 4s (right shift)
starCaptureRadius = 1, // str 01 (greater zero)
collisionRadius = 48, // me1 06000 (screen coors)
collisionRadius2 = 24, // me2 03000 (above/2)
torpedoSpaceWarpage = 9, // the sar 9s (right shift)
hyperspaceShots = 8, // mhs 010 (number of)
hyperspaceTimeBeforeBreakout = 32, // hd1 040 (frames)
hyperspaceTimeInBreakout = 64, // hd2 0100 (frames)
hyperspaceRechargeTime = 128, // hd3 0200 (frames)
hyperspaceDisplacement = 9, // hr1 scl 9s (left shift)
hyperspaceInducedVelocity = 4, // hr2 scl 4s (left shift)
hyperspcaceUncertancy = 16384; // hur 040000 (threshold bonus)
// ship outline codes
var outline1 = [ // spaceship 1 (needle)
1, 1, 1, 1, 3, 1,
1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 6, 3,
3, 1, 1, 1, 1, 1,
1, 4, 6, 1, 1, 1,
1, 1, 1, 1, 1, 4,
7, 0, 0, 0, 0, 0
],
outline2 = [ // spaceship 2 (wedge)
0, 1, 3, 1, 1, 3,
1, 1, 3, 1, 1, 1,
1, 1, 6, 3, 1, 3,
1, 3, 1, 1, 1, 1,
1, 6, 1, 1, 5, 1,
1, 1, 1, 6, 3, 3,
3, 6, 5, 1, 1, 4,
7, 0, 0, 0, 0, 0
];
/*
the screen is represented internally like the DEC type 30 display:
1048 x 1048 at 7 intensities, origin at the center.
while the original uses a fixed point format (fx10.18) for positions,
floats are used in the JS implementation.
intensities are 3 bit values, one's complement (+3 .. -3).
higher intensities also cause bigger spot-sizes (blips).
the original display is a point plotted device (animated display),
meaning there's no memory or frame buffer: blips will be activated
and fade away (in the afterglow of the Type 30 CRT's P7 phospor).
co-ordinates are as in the PDP-1 with positive axis up and right:
+512
-511 0/0 +512
-511
*/
// display constants (for readability)
var SCREENWIDTH = 1024,
COORS_MAX = SCREENWIDTH/2,
QUADRANT = COORS_MAX/2;
// dictionary for controls (exported as property)
var Controls = {
SPACESHIP1: 0,
SPACESHIP2: 1,
LEFT: 8,
RIGHT: 4,
THRUST: 2,
FIRE: 1,
HYPERSPACE: 12, // left + right
RESET: 15, // use to clear all
ALL: 15,
legalInputs: { 1: true, 2: true, 4: true, 8: true, 12: true, 15: true }
};
// static vars
var mtb = [], // table of objects
nob = 24, // number of objects
score1 = 0, // score spaceship 1
score2 = 0, // score spaceship 2
testword = 0,
timer = 0,
restartCounter = 0,
gameCounter = 0,
halted = false;
/*
The testword (var testword) represents the state of an array of switches at
the operator's console for 18-bit input.
If Options.TESTWORDCONTROLS is set, input propagates to spaceship controls
as follows: "high order 4 bits, rotate ccw, rotate cw, (both mean hyperspace)
fire rocket, and fire torpedo. Low order 4 bits, same for other ship."
-- player 1: bits 17 .. 14 (left, right, thrust, fire)
-- player 2: bits 3 .. 0 (left, right, thrust, fire)
bits 5 .. 10 are related to scoring (bits 6 .. 10: number of games per match)
bits: 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
usage: -PLAYER 1- GAMES P. MATCH SC -PLAYER 2-
semantics: L R T F 16 8 4 2 1 L R T F
| | => 0..31 GAMES | |
HYPERSPACE HYPERSPACE
SC: Show scores in single game mode or after the next game in match mode.
(Here, scores will be displayed anyway.) If cleared, when resumed from a
halt for the score display, scores are reset.
*/
// constructors (symbols/pointers of the original program in parentheses)
function CollidibleObject() {
this.handler = null; // (symbol: mtb, pointer: ml1)
this.collidible = false; // (sign-bit in handler address)
this.x = 0; // pos x (nx1, mx1)
this.y = 0; // pos y (ny1, my1)
this.dx = 0; // delta x (ndx, mdx)
this.dy = 0; // delta y (ndy, mdy)
this.counter = 0; // time of torpedo or explosion (na1, ma1)
this.size = 0; // used for explosions (nb1, mb1)
}
function Spaceship() { // extends CollidibleObject
this.angularMomentum = 0; // (symbol: nom, pointer: mom)
this.theta = 0; // rotation (nth, mth)
this.fuel = 0; // amount of fuel (nfu, mfu)
this.torpedoes = 0; // torpedoes left (ntr, mtr)
this.outline = null; // outline code (not, mot)
this.hyp1 = 0; // hyperspace: handler backup (nh1, mh1)
this.hyp2 = 0; // hyperspace jumps remaining (nh2, mh2)
this.hyp3 = 0; // hyperspace cooling (nh3, mh3)
this.hyp4 = 0; // hyperspace uncertainty (nh4, mh4)
this.ctrl = 0; // control input (pntr cwg)
this.lastCtrl = 0; // last control word (nco, mco)
}
Spaceship.prototype = new CollidibleObject();
// main -- start a game
function run( options ) {
// initialize any sense switch flags (optional)
// e.g., Spacewar.run( { SUNKILLS: true } );
if (typeof options === 'object') {
for (var k in options) setOption(k, options[k]);
}
// start the game
ExpensivePlanetarium.reset();
restartCounter = 0;
frame();
if (timer) clearInterval(timer);
timer = setInterval(frame, Math.floor(1000/Options.FPS));
}
function newGame() { /* (label a40) */
var i, ss1, ss2;
// clear and init table of objects (label a2)
mtb.length = 0;
for (i = 0; i < 2; i++) mtb.push( new Spaceship() );
for (i = 2; i < nob; i++) mtb.push( new CollidibleObject() );
// setup spaceships (label a2, a3)
ss1 = mtb[0];
ss2 = mtb[1];
ss1.handler = spaceshipHandler;
ss1.collidible = true;
ss2.handler = spaceshipHandler;
ss2.collidible = true;
ss1.x = QUADRANT;
ss1.y = QUADRANT;
ss2.x = -QUADRANT;
ss2.y = -QUADRANT;
ss1.theta = Math.PI;
ss2.theta = 0;
ss1.outline = compileOutline(outline1);
ss2.outline = compileOutline(outline2);
ss1.torpedoes = ss2.torpedoes = torpedoSupply;
ss1.fuel = ss2.fuel = fuelSupply;
ss1.hyp2 = ss2.hyp2 = hyperspaceShots;
// explosion size will be derived from this (orig: instruction count)
ss1.size = ss2.size = 1024;
}
function frame() { /* (label a) */
var ss1, ss2, thriving1, thriving2, endOfMatch = false;
if (!halted) {
// end of game and restart checks, executed at start of each frame
// (these were external patches in version 2b)
if (restartCounter == 0) { /* (label a6) */
// here after halt (scores display) or at first run
if ((testword & 32) == 0) {
// clear scores on testword bit 5 zero
score1 = score2 = 0;
displayScores(); // notify UI, if found
// read new number of games for match play (0 .. 31)
gameCounter = (testword >>> 6) & 31;
}
newGame();
}
// check, if ships are alive and have any torpedoes left
ss1 = mtb[0];
ss2 = mtb[1];
thriving1 = (ss1.handler === spaceshipHandler);
thriving2 = (ss2.handler === spaceshipHandler);
if (thriving1 && thriving2
&& (ss1.torpedoes > 0 || ss2.torpedoes > 0)) {
// reset restart-counter
restartCounter = 2 * torpedoLife;
}
else if (--restartCounter == 0) { // count down to scoring
// count-down reached: whoever is still alive is awarded a score
if (thriving1) score1 = (score1 + 1) % 0x3FFFF; // 18 bit overflow
if (thriving2) score2 = (score2 + 1) % 0x3FFFF;
displayScores(); // display scores anyway (original see below)
// check match-play
if (gameCounter > 0) {
if (--gameCounter == 0) { // count down
// end of a match
if (score1 == score2) {
// it's a tie, one more game
gameCounter = 1;
}
else {
endOfMatch = true;
}
}
}
// halt (to show scores), if match over or bit 5 in testword set,
// else start over
if (endOfMatch || (testword & 32)) {
// original puts score 1 into AC and score 2 in IO and halts
// displayScores();
if (Options.HALTONSCORES) {
halted = true;
haltSignal(); // notify UI, if found
}
return; // we'll resume at the top, since restartCounter is still zero
}
else {
// no special case, restart the game
newGame();
restartCounter = 1; // not in original (fix resume-after-halt logic)
}
}
// finally advance to the main loop ...
// (in the original program input is read at the start of each spaceship handler)
// read special input (normally expected to be set by 'Spacewar.setControls()')
if (Options.TESTWORDCONTROLS) {
// map testword bits to spaceship controls
mtb[0].ctrl = (testword >> 14) & 15;
mtb[1].ctrl = testword & 15;
}
readGamepads();
mainLoop();
}
CRT.update(); // external UI, not in original
}
function mainLoop() { /* (label ml0, ml1) */
var obj1, obj2, nnn = nob - 1;
// loop over objects
for (var i = 0; i < nnn; i++) {
obj1 = mtb[i];
// is it active?
if (obj1.handler) {
// can it colide?
if (obj1.collidible) {
// comparison loop
for (var j = i + 1; j < nob; j++) {
obj2 = mtb[j];
// collidible?
if (obj2.collidible) {
// evaluate object distances by an octogonal hitbox or proximity
var dx = Math.abs(obj1.x - obj2.x);
if (dx < collisionRadius) {
var dy = Math.abs(obj1.y - obj2.y);
if (dy < collisionRadius && dx + dy < collisionRadius2) {
// explode
obj1.handler = obj2.handler = explosionHandler;
obj1.collidible = obj2.collidible = false;
// set up explosion time & size
obj1.counter = obj2.counter = (obj1.size + obj2.size - 1) >> 8;
}
}
}
}
}
// call the object's method
obj1.handler();
}
}
// handle last object, if any
obj1 = mtb[nnn];
if (obj1.handler) obj1.handler();
if (!Options.NOBACKGROUND) ExpensivePlanetarium.update(); // background stars
if (!Options.SUNOFF) drawHeavyStar(); // gravtational star ("sun")
}
// object handlers, this-object is current object (see mainLoop)
function spaceshipHandler() { /* (label sr0) */
var am, sin, cos, bx, by, t1, t2,
sx1, sy1, stx, sty, p,
scn, ssc, ssm, ssn, ssd, csn, csm, scm,
src, torp, m, f,
thrusting = false;
// rotation
am = this.angularMomentum;
if (this.ctrl & Controls.LEFT) am += angularAcceleration;
if (this.ctrl & Controls.RIGHT) am -= angularAcceleration;
if (Options.ANGULARMOMENTUM) {
this.angularMomentum = am;
}
else {
this.angularMomentum = 0;
am *= 128; // 1<<7
}
this.theta += am;
// limit to +/- 2*PI
if (this.theta > TAU) {
this.theta -= TAU;
}
else if (this.theta < -TAU) {
this.theta += TAU;
}
sin = Math.sin(this.theta);
bx = by = 0;
// gravity computations
if (!Options.SUNOFF) {
t1 = this.x / 8;
t2 = this.y / 8;
t1 = t1 * t1 + t2 * t2;
if (t1 < starCaptureRadius) { // in sun (label pof)
this.dx = this.dy = 0;
if (Options.SUNKILLS) { // explode (label po1)
this.handler = explosionHandler;
this.collidible = false;
this.counter = 8;
}
else { // set ship to "anti pode"
this.x = this.y = COORS_MAX;
}
return;
}
t1 = (Math.sqrt(t1) * t1) / 2;
if (!Options.LOWGRAVITY) t1 /= 4;
bx = -this.x / t1;
by = -this.y / t1;
}
// ... and back to business ...
cos = Math.cos(this.theta);
// rockets fired?
if ((this.ctrl & Controls.THRUST) && this.fuel) {
f = 1 << spaceshipAcceleration; // use div instead of right shift
by += cos / f;
bx -= sin / f;
thrusting = true;
}
// update positions
this.dy += by;
this.y += this.dy / 8;
this.dx += bx;
this.x += this.dx / 8;
toroidalize(this);
// half a ship's length
ssn = sin * 16;
scn = cos * 16;
// outline start pos (stern, ahead of center)
sx1 = this.x - ssn;
sy1 = this.y + scn;
// torpedoes will show up here
stx = sx1 - ssn;
sty = sy1 + scn;
// draw the ship and update drawing pos to end of outline
ssn = sin;
scn = cos;
ssm = ssn;
ssc = ssn + scn;
ssd = ssc;
csn = ssn - scn;
csm = -csn;
scm = scn;
p = this.outline(sx1, sy1, ssn, scn, ssm, ssc, ssd, csn, csm, scm);
sx1 = p.x;
sy1 = p.y;
// draw exhausts
if (thrusting) {
src = Math.floor(Math.random() * 16);
ssn = sin * 2;
scn = cos * 2;
// fuel consumption is a function of the blast's length!
while (this.fuel > 0 && --src > 1) {
this.fuel--;
sx1 += ssn;
sy1 -= scn;
plot (sx1, sy1, 0);
}
}
if (this.counter > 0) { // torpedo cooling
this.counter--;
}
else if ( // fire, no single-shot-lock, and torpedoes left?
(this.ctrl & Controls.FIRE)
&& (!Options.SINGLESHOTS || !(this.lastCtrl & Controls.FIRE))
&& this.torpedoes
) {
this.torpedoes--;
// find empty object and set up the torpedo
for (m = 2; m < nob; m++) {
if (!mtb[m].handler) {
torp = mtb[m];
torp.handler = torpedoHandler;
torp.collidible = true;
torp.x = stx;
torp.y = sty;
f = 1 << torpedoVelocity; // use div instead of right shift
torp.dx = this.dx - sin * 512 / f;
torp.dy = this.dy + cos * 512 / f;
torp.size = 16;
torp.counter = torpedoLife;
this.counter = torpedoReloadTime;
break;
}
}
}
// hyperspace
if (this.hyp3 > 0) { // cooling
this.hyp3--;
}
else if (this.hyp2 > 0) { // jumps remaining?
// are controls for left and right set and was neither of them set before?
// (last condition is thought to inhibit accidental jumps.
// ignored in original, since the last control word is never saved,
// works out as "if (this.ctrl) & Controls.HYPERSPACE)".)
if ((((~this.ctrl) | this.lastCtrl) & Controls.HYPERSPACE) == 0) {
this.hyp1 = this.handler;
this.handler = hyperspaceHandler;
this.collidible = false;
this.counter = hyperspaceTimeBeforeBreakout;
// this.size = 3; // not used here
}
}
// store last control word (missing in original)
this.lastCtrl = this.ctrl;
}
function toroidalize(obj) {
// util for toroidal space (in original maintained by word length)
if (obj.x <= -COORS_MAX) {
obj.x += SCREENWIDTH;
}
else if (obj.x > COORS_MAX) {
obj.x -= SCREENWIDTH;
}
if (obj.y <= -COORS_MAX) {
obj.y += SCREENWIDTH;
}
else if (obj.y > COORS_MAX) {
obj.y -= SCREENWIDTH;
}
}
function explosionHandler() { /* (label mex) */
var x, y, mxc, f;
this.y += this.dy / 8;
this.x += this.dx / 8;
toroidalize(this);
// particles
mxc = this.size >> 3;
do {
/*
algorithm:
1) set up number of right shifts: (mxc > 96)? 1:3
2) set up number of left shifts: random number int 0..9
(shifts apply to a combined 36-bit register, 18 bits x and y each,
sign preserved in x. [x = AC, y = IO])
3) set x and y to signed 9-bit random numbers
4) apply right shifts (combined registers)
5) apply left shifts (combined registers)
6) add to position and display it
(only bits 17..9 (hsb-first) are significant for co-ordinates)
using floats, we apply mult/div instead of shifts:
mult/div factors are 1 << n.
any sign flips in y are ignored (just a random number).
*/
f = (1 << Math.floor(Math.random() * 9)) / ((mxc > 96)? 2:8);
x = (Math.random() - 0.5) * 2 * f;
y = (Math.random() - 0.5) * 2 * f;
plot(this.x + x, this.y + y, 3);
} while (--mxc > 0);
if (--this.counter <= 0) this.handler = null;
}
function torpedoHandler() { /* (label trc) */
if (--this.counter < 0) {
// time fuse
this.counter = 2;
this.handler = explosionHandler;
this.collidible = false;
}
else { /* (label t1c) */
this.dy += this.x / (512 * (1 << torpedoSpaceWarpage));
this.y += this.dy / 8;
this.dx += this.y / (512 * (1 << torpedoSpaceWarpage));
this.x += this.dx / 8;
toroidalize(this);
plot(this.x, this.y, 1);
}
}
function hyperspaceHandler() {
// "this routine handles a non-colliding ship invisibly in hyperspace"
if (--this.counter == 0) { // spend time in hyperspace ...
// zero, set up next step
this.handler = hyperspaceHandler2;
// this.size = 7; // not used here
// set up displacement
this.x += (Math.random() * 2 - 1) * (1 << hyperspaceDisplacement);
this.y += (Math.random() * 2 - 1) * (1 << hyperspaceDisplacement);
toroidalize(this); // maintain toroidal space (not in original)
// add induced velocity
this.dx += (Math.random() * 2 - 1) * (1 << hyperspaceInducedVelocity);
this.dy += (Math.random() * 2 - 1) * (1 << hyperspaceInducedVelocity);
// set up a random rotation
this.theta = TAU * Math.random();
// original adds some instructions to keep it in bounds of 0 .. 2 PI
this.counter = hyperspaceTimeInBreakout;
}
}
function hyperspaceHandler2() {
// "this routine handles a ship breaking out of hyperspace"
if (--this.counter > 0) {
// spend time in breakout, display a blip
plot(this.x, this.y, 2);
}
else {
// zero, now check, restore spaceship handler
this.handler = this.hyp1;
this.collidible = true;
this.size = 1024;
if (this.hyp2 > 0) this.hyp2--; // decrement remaining jumps
this.hyp3 = hyperspaceRechargeTime;
// now check, if we break on re-entry (Mark One Hyperfield Generators ...)
this.hyp4 += hyperspcaceUncertancy;
var r = ((Math.random() * (1 << 20)) | 0) & 0x1FFFF; // 17-bits random
if (this.hyp4 >= r) { // explode
this.handler = explosionHandler;
this.collidible = false;
this.counter = 10;
}
}
}
// outline compiler, returns anonymous function
// variables are rather passed to the resulting function as arguments than shared as globals
// when called: f(sx1, sy1, ssn, scn, ssm, ssc, ssd, csn, csm, scm)
// generated function returns object { "x": x, "y": y },
// where x, y are the coors of the last plot position to be used for sx1 and sy1 respectively.
function compileOutline(outline) { // (label oc)
var i = 0,
scan = true,
max = outline.length,
oc; // oc: function body of compiled outline code
function comtab(a, b) {
oc += 'x += ' + a + ';\n' +
'y += ' + b + ';\n' +
'plot(x, y, 0);\n';
}
// compile the function body for plotting the given outline
// the compiled code represents an unrolled loop in two passes for drawing a side a time
// code 7 marks the end of the code and initiates the second pass to draw the other side
// local values passed as arguments:
// sx1, sy1: spaceship coors;
// ssn, scn, ssm, ssc, ssd, csn, csm, scm: matrix of offsets derived from unit vector
oc = 'var x, y, stored, xs, ys, t, secondPass = false;\n' +
'for (;;) {\n' +
'x = sx1; y = sy1; stored = false;\n';
while (scan) {
switch (outline[i++]) {
case 0: // fall through
case 1: // advance to rear and plot
comtab('ssn', '-scn');
break;
case 2: // advance outwards and plot
comtab('scm', 'ssm');
break;
case 3: // advance to rear and outwards and plot
comtab('ssc', '-csm');
break;
case 4: // advance inwards and plot
comtab('-scm', '-ssm');
break;
case 5: // advance to rear and inwards and plot
comtab('csn', '-ssd');
break;
case 6: // store/restore position
oc += 'if (stored) { x = xs; y = ys; } else { xs = x; ys = y; }\n' +
'stored = !stored;\n';
break;
case 7: // end of code: do mirrored second pass or break out to return
oc += 'if (secondPass) break;\n' +
'scm = -scm;\n' +
'ssm = -ssm;\n' +
't = csm;\n' +
'csm = ssd\n;' +
'ssd = t;\n' +
't = ssc;\n' +
'ssc = csn;\n' +
'csn = t;\n' +
'secondPass = true;\n';
scan = false;
break;
}
if (scan && i === max) { // warn and fix corrupted outline (not in original)
console.warn('Outline Compiler: Outline without code 7!');
outline.push(7);
}
}
// code to close the loop executing a pass and to return the last plotting position
oc += '}\n' +
'return { "x": x, "y": y };\n';
// return the generated function
return eval( '(function(sx1, sy1, ssn, scn, ssm, ssc, ssd, csn, csm, scm) {' + oc + '});' );
}
/*
// alternate outline interpreter, takes array of outline codes as the first argument
function drawOutline(oc, sx1, sy1, ssn, scn, ssm, ssc, ssd, csn, csm, scm) {
// (lable oc => properties 'not', 'not+1')
var pass1 = true,
mark = false,
x = sx1,
y = sx2,
i = 0,
max = oc.length,
mx, my, t;
while (i < max) {
var x1=x, y1=y;
switch (oc[i++]) {
case 0: // fall through
case 1: // down
x += ssn;
y -= scn;
plot(x, y, 0);
break;
case 2: // right
x += scm;
y += ssm;
plot(x, y, 0);
break;
case 3: // down right
x += ssc;
y -= csm;
plot(x, y, 0);
break;
case 4: // left
x -= scm;
y -= ssm;
plot(x, y, 0);
break;
case 5: // down left
x += csn;
y -= ssd;
plot(x, y, 0);
break;
case 6: // store/restore position
if (mark) {
x = mx;
y = my;
}
else {
mx = x;
my = y;
}
mark = !mark;
break;
case 7: // mirror / return
if (pass1) {
// flip matrix horizontally
scm = -scm;
ssm = -ssm;
t = csm;
csm = ssd;
ssd = t;
t = ssc;
ssc = csn;
csn = t;
// start over
i = 0;
x = sx1;
y = sx2;
pass1 = false;
mark = false; // fix any orphaned codes 6 (not in original)
}
else {
// return last plotting position
return {
'x': x,
'y': y
};
}
break;
}
}
// failsafe return -- not in original, we should have returned at code 7 before!
return {
'x': x,
'y': y
};
}
*/
// draw the gravitational star
function drawHeavyStar() { // (label blp)
var x = 0,
y = 0,
bx = Math.random() * 2 -1,
by = Math.random() * 2 -1,
l = 16 - Math.floor(Math.random()*8),
i;
function starp() {
x += bx;
y += by;
plot(x, y, 0);
}
plot(x, y, 0);
for (i = 0; i < l; i++) starp();
x = -x;
y = -y;
for (i = 0; i < l; i++) starp();
}
// the program in the program ...
var ExpensivePlanetarium = (function() {
"use strict";
// "background display 3/13/62, prs."
// config
var NOSKIP = false, // opt out of skip of every 2nd frame
INTERLACED = false; // Spacewar 2b - mode
// static vars
var m1, // Magnitude('1j') (defined below the star data)
m2, // Magnitude('2j')
m3, // Magnitude('3j')
m4, // Magnitude('4j')
bcc = 0, // frame skip counter (spacewar 3.1 mode)
bkc = 0, // step counter
fpr = 4096; // (010000) right frame margin, map center
// dimensional constants (for readability)
var SCREEN_WIDTH = 1024,
HALFSCREEN = SCREEN_WIDTH/2,
MAP_WIDTH = 8192; // range of x-domain (0 .. 020000)
// constructor, in original an assembler macro
function Magnitude(label) {
var data = stars[label];
this.coors = []; // star data
this.cursor = 0; // data cursor (label flo, initialized to offset J)
// transform coors (like macro "mark")
for (var i = 0; i < data.length; i+=2) {
// x-coor: max - x (flip horizontally)
this.coors.push(MAP_WIDTH - data[i]);
// y-coor here as is (orig. prepares for display instructions)
this.coors.push(data[i + 1]);
}
data = null;
}
Magnitude.prototype.dislis = function(b) { /* (macro dislis) */
// scans stars, displays on-screen ones,
// limits scan per frame by cursor-pos. (stars are ordered by x-coors)
// the original implementation loops over a subset of the entire set
// of coordinates, in bounds of offsets J and Q+2 (Q: index of last x)
// here, coors are private, J is always zero and Q+2 is coors.length.
// argument b: brightness (display intensity).
var didPlot = false, // (program flag 5)
startIdx = this.cursor, // data index on entry (label fpo)
cx = this.cursor, // data pos x-coor (label fin)
cy = this.cursor + 1, // data pos y-coor (label fyn)
sx; // screen x-coor (just AC in original)
for (;;) { // loop over coors (label fin)
// prepare the screen x-coordinate for display
sx = this.coors[cx] - fpr; // x-coor - right margin
// check, if inbounds to the right
if (sx >= 0) { // off-screen, try wrap/overlap (label fgr)
sx = -MAP_WIDTH + SCREEN_WIDTH;
}
else {
sx += SCREEN_WIDTH;
}
// check, if inbounds to the left (label frr, fou)
if (sx <= 0) { // off-screen to the left (label fuu)
// did we plot any?
if (didPlot) break;
// since the view moves from right to left, we won't inspect it in
// the next run again, advance the data cursor (start with next star)
this.cursor += 2;
// the original loops over offsets J .. Q+2
if (this.cursor == this.coors.length) this.cursor = 0;
}
else { // on-screen (0 >= sx > SCREEN_WIDTH)
// display it (label fie)
plot(sx - HALFSCREEN, this.coors[cy], b); // in main
didPlot = true;
}
// next star (label fid)
cy++;
if (cy == this.coors.length) { // data wrap-around (label flp; length: Q+2)
// did we start at 0 (have we seen all)?
if (startIdx == 0) break; // in original offset J, here always zero
// no, start over with first star
cx = 0; // in original offset J, here always zero
cy = 1; // in original J+1
}
else {
// have we performed a full wrap (seen all)?
if (cy == startIdx) break;
// continue with next star (next pair of coors)
cx = cy;
cy++;
}
}
};
function update() { /* main method ( label bck ) */
// two implementations: once as in sw 3.1 and once as in sw 2b
if (!INTERLACED) {
// spacewar 3.1: stars modulated by display intensities
// displays every second frame only (opt-out option not in original code)
if (!NOSKIP) {
if (++bcc < 0) return;
bcc = -2;
}
m1.dislis(3);
m2.dislis(2);
m3.dislis(1);
m4.dislis(0);
// advance every 32nd frame (every 16th display frame)
if (++bkc >= 0) {
bkc = (NOSKIP)? -32 : -16;
fpr--;
if (fpr < 0) fpr += MAP_WIDTH; // reset right margin (8192)
}
}
else {
// spacewar 2b: stars modulated by frequency of update
// (requires emulation of P7 phosphor, speed options not implemented)
m1.dislis(0);
bcc++;
if (bcc & 1) m3.dislis(0);
m2.dislis(0);
if (bcc & 3 != 0) m4.dislis(0);
m1.dislis(0); // display first magnitude again (brightest)
bcc %= 4; // not in the original code (wraps around by overflow)
// advance every 32nd frame
if (++bkc >= 0) {
bkc = -32;
fpr--;
if (fpr < 0) fpr += MAP_WIDTH; // reset right margin
}
}
}
// API methods (exported)
function setOption(key, v) {
var k = String(key).toUpperCase().replace(/[^A-Z0-9]/g, '');
switch (k) {
case 'FRAMESKIP':
v = !v; // fall through
case 'NOSKIP':
if (NOSKIP && bkc < -16) bkc += 16;
NOSKIP = Boolean(v);
break;
case 'INTERLACED':
case 'SPACEWAR2B':
INTERLACED = Boolean(v);
bcc = bkc = 0;
break;
}
}
function reset() {
fpr = 4096;
bcc = bkc = 0;
}
/*
star data: "stars by prs 3/13/62 for s/w 2b"
stars of 1st to 4th magnitude (1j .. 4j) of segment 22.5 deg N to 22.5 deg S
data order: x, y (ascending by x), x-range: 0 .. 8192, y-range: -511 .. +512
units: screen coors, x: offset from right margin, y: N (top) < 0 < S (bottom)
(y-domain scaled to internal screen representation, x-domain proportional.)
*/
var stars = {
'1j': [
1537, 371, // 87 Taur, Aldebaran
1762, -189, // 19 Orio, Rigel
1990, 168, // 58 Orio, Betelgeuze
2280, -377, // 9 CMaj, Sirius
2583, 125, // 10 CMin, Procyon
3431, 283, // 32 Leon, Regulus
4551, -242, // 67 Virg, Spica
4842, 448, // 16 Boot, Arcturus
6747, 196 // 53 Aqil, Altair
],
'2j': [
1819, 143, // 24 Orio, Bellatrix
1884, -29, // 46 Orio
1910, -46, // 50 Orio
1951, -221, // 53 Orio
2152, -407, // 2 CMaj
2230, 375, // 24 Gemi
3201, -187, // 30 Hyda, Alphard
4005, 344, // 94 Leon, Denebola
5975, 288 // 55 Ophi
],
'3j': [
46, 333, // 88 Pegs, Algenib
362, -244, // 31 Ceti
490, 338, // 99 Pisc
566, -375, // 52 Ceti
621, 462, // 6 Arie
764, -78, // 68 Ceti, Mira
900, 64, // 86 Ceti
1007, 84, // 92 Ceti
1243, -230, // 23 Erid
1328, -314, // 34 Erid
1495, 432, // 74 Taur
1496, 356, // 78 Taur
1618, 154, // 1 Orio
1644, 52, // 8 Orio
1723, -119, // 67 Erid
1755, -371, // 5 Leps
1779, -158, // 20 Orio
1817, -57, // 28 Orio
1843, -474, // 9 Leps
1860, -8, // 34 Orio
1868, -407, // 11 Leps
1875, 225, // 39 Orio
1880, -136, // 44 Orio
1887, 480, // 123 Taur
1948, -338, // 14 Leps
2274, 296, // 31 Gemi
2460, 380, // 54 Gemi
2470, 504, // 55 Gemi
2513, 193, // 3 CMin
2967, 154, // 11 Hyda
3016, 144, // 16 Hyda
3424, 393, // 30 Leon
3496, 463, // 41 Leon, Algieba
3668, -357, // nu Hyda
3805, 479, // 68 Leon
3806, 364, // 10 Leon
4124, -502, // 2 Corv
4157, -387, // 4 Corv
4236, -363, // 7 Corv
4304, -21, // 29 Virg
4384, 90, // 43 Virg
4421, 262, // 47 Virg
4606, -2, // 79 Virg
4721, 430, // 8 Boot
5037, -356, // 9 Libr
5186, -205, // 27 Libr
5344, 153, // 24 Serp
5357, 358, // 28 Serp
5373, -71, // 32 Serp
5430, -508, // 7 Scor
5459, -445, // 8 Scor
5513, -78, // 1 Ophi
5536, -101, // 2 Ophi
5609, 494, // 27 Herc
5641, -236, // 13 Ophi
5828, -355, // 35 Ophi
5860, 330, // 64 Herc
5984, -349, // 55 Serp
6047, 63, // 62 Ophi
6107, -222, // 64 Ophi
6159, 217, // 72 Ophi
6236, -66, // 58 Serp
6439, -483, // 37 Sgtr
6490, 312, // 17 Aqil
6491, -115, // 16 Aqil
6507, -482, // 41 Sgtr
6602, 66, // 30 Aqil
6721, 236, // 50 Aqil
6794, 437, // 12 Sgte
6862, -25, // 65 Aqil
6914, -344, // 9 Capr
7014, 324, // 6 Dlph
7318, -137, // 22 Aqar
7391, 214, // 8 Pegs
7404, -377, // 49 Capr
7513, -18, // 34 Aqar
7539, 130, // 26 Pegs
7644, -12, // 55 Aqar
7717, 235, // 42 Pegs
7790, -372, // 76 Aqar
7849, 334 // 54 Pegs, Markab
],
'4j': [
1, -143, // 33 Pisc
54, 447, // 89 Pegs
54, -443, // 7 Ceti
82, -214, // 8 Ceti
223, -254, // 17 Ceti
248, 160, // 63 Pisc
273, -38, // 20 Ceti
329, 167, // 71 Pisc
376, 467, // 84 Pisc
450, -198, // 45 Ceti
548, 113, // 106 Pisc
570, 197, // 110 Pisc
595, -255, // 53 Ceti
606, -247, // 55 Ceti
615, 428, // 5 Arie
617, 61, // 14 Pisc
656, -491, // 59 Ceti
665, 52, // 113 Pisc
727, 191, // 65 Ceti
803, -290, // 72 Ceti
813, 182, // 73 Ceti
838, -357, // 76 Ceti
878, -2, // 82 Ceti
907, -340, // 89 Ceti
908, 221, // 87 Ceti
913, -432, // 1 Erid
947, -487, // 2 Erid
976, -212, // 3 Erid
992, 194, // 91 Ceti
1058, 440, // 57 Arie
1076, 470, // 58 Arie
1087, -209, // 13 Erid
1104, 68, // 96 Ceti
1110, -503, // 16 Erid
1135, 198, // 1 Taur
1148, 214, // 2 Taur
1168, 287, // 5 Taur
1170, -123, // 17 Erid
1185, -223, // 18 Erid
1191, -500, // 19 Erid
1205, 2, // 10 Taur
1260, -283, // 26 Erid
1304, -74, // 32 Erid
1338, 278, // 35 Taur
1353, 130, // 38 Taur
1358, 497, // 37 Taur
1405, -162, // 38 Erid
1414, 205, // 47 Taur
1423, 197, // 49 Taur
1426, -178, // 40 Erid
1430, 463, // 50 Taur
1446, 350, // 54 Taur
1463, 394, // 61 Taur
1470, 392, // 64 Taur
1476, 502, // 65 Taur
1477, 403, // 68 Taur
1483, 350, // 71 Taur
1485, 330, // 73 Taur
1495, 358, // 77 Taur
1507, 364, //
1518, -6, // 45 Erid
1526, 333, // 86 Taur
1537, 226, // 88 Taur
1544, -81, // 48 Erid
1551, 280, // 90 Taur
1556, 358, // 92 Taur
1557, -330, // 53 Erid
1571, -452, // 54 Erid
1596, -78, // 57 Erid
1622, 199, // 2 Orio
1626, 124, // 3 Orio
1638, -128, // 61 Erid
1646, 228, // 7 Orio
1654, 304, // 9 Orio
1669, 36, // 10 Orio
1680, -289, // 64 Erid
1687, -167, // 65 Erid
1690, -460, //
1690, 488, // 102 Taur
1700, 347, // 11 Orio
1729, 352, // 15 Orio
1732, -202, // 69 Erid
1750, -273, // 3 Leps
1753, 63, // 17 Orio
1756, -297, // 4 Leps
1792, -302, // 6 Leps
1799, -486, //
1801, -11, // 22 Orio
1807, 79, // 23 Orio
1816, -180, // 29 Orio
1818, 40, // 25 Orio
1830, 497, // 114 Taur
1830, 69, // 30 Orio
1851, 134, // 32 Orio
1857, 421, // 119 Taur
1861, -168, // 36 Orio
1874, 214, // 37 Orio
1878, -138, //
1880, -112, // 42 Orio
1885, 210, // 40 Orio
1899, -60, // 48 Orio
1900, 93, // 47 Orio
1900, -165, // 49 Orio
1909, 375, // 126 Taur
1936, -511, // 13 Leps
1957, 287, // 134 Taur
1974, -475, // 15 Leps
1982, 461, // 54 Orio
2002, -323, // 16 Leps
2020, -70, //
2030, 220, // 61 Orio
2032, -241, // 3 Mono
2037, 458, // 62 Orio
2057, -340, // 18 Leps
2059, 336, // 67 Orio
2084, 368, // 69 Orio
2084, 324, // 70 Orio
2105, -142, // 5 Mono
2112, -311, //
2153, 106, // 8 Mono
2179, 462, // 18 Gemi
2179, -107, // 10 Mono
2184, -159, // 11 Mono
2204, 168, // 13 Mono
2232, -436, // 7 CMaj
2239, -413, // 8 CMaj
2245, -320, //
2250, 227, // 15 Mono
2266, 303, // 30 Gemi
2291, 57, // 18 Mono
2327, 303, // 38 Gemi
2328, -457, // 15 CMaj
2330, -271, // 14 CMaj
2340, -456, // 19 CMaj
2342, -385, // 20 CMaj
2378, -93, // 19 Mono
2379, 471, // 43 Gemi
2385, -352, // 23 CMaj
2428, -8, // 22 Mono
2491, -429, //
2519, 208, // 4 CMin
2527, 278, // 6 CMin
2559, -503, //
2597, -212, // 26 Mono
2704, -412, //
2709, -25, // 28 Mono
2714, 60, //
2751, -61, // 29 Mono
2757, -431, // 16 Pupp
2768, -288, // 19 Pupp
2794, 216, // 17 Canc
2848, -82, //
2915, 138, // 4 Hyda
2921, 84, // 5 Hyda
2942, -355, // 9 Hyda
2944, 497, // 43 Canc
2947, 85, // 7 Hyda
2951, -156, //
2953, 421, // 47 Canc
2968, -300, // 12 Hyda
2976, 141, // 13 Hyda
3032, 279, // 65 Canc
3124, 62, // 22 Hyda
3157, -263, // 26 Hyda
3161, -208, // 27 Hyda
3209, -53, // 31 Hyda
3225, -17, // 32 Hyda
3261, 116, //
3270, -16, // 35 Hyda
3274, -316, // 38 Hyda
3276, 236, // 14 Leon
3338, -327, // 39 Hyda
3385, 194, // 29 Leon
3415, -286, // 40 Hyda
3428, 239, // 31 Leon
3429, 3, // 15 Sext
3446, -270, // 41 Hyda
3495, 455, // 40 Leon
3534, -372, // 42 Hyda
3557, -3, // 30 Sext
3570, 223, // 47 Leon
3726, -404, // al Crat
3736, -44, // 61 Leon
3738, 471, // 60 Leon
3754, 179, // 63 Leon
3793, -507, // 11 Crat
3821, -71, // 74 Leon
3836, -324, // 12 Crat
3846, 150, // 77 Leon
3861, 252, // 78 Leon
3868, -390, // 15 Crat
3935, -211, // 21 Crat
3936, -6, // 91 Leon
3981, -405, // 27 Crat
3986, 161, // 3 Virg
3998, 473, // 93 Leon
4013, 53, // 5 Virg
4072, 163, // 8 Virg
4097, 211, // 9 Virg
4180, -3, // 15 Virg
4185, 418, // 11 Coma
4249, -356, // 8 Corv
4290, -170, // 26 Virg
4305, 245, // 30 Virg
4376, -205, // 40 Virg
4403, 409, // 36 Coma
4465, -114, // 51 Virg
4466, 411, // 42 Coma
4512, -404, // 61 Virg
4563, -352, // 69 Virg
4590, -131, // 74 Virg
4603, 95, // 78 Virg
4679, 409, // 4 Boot
4691, 371, // 5 Boot
4759, 46, // 93 Virg
4820, 66, //
4822, -223, // 98 Virg
4840, -126, // 99 Virg
4857, -294, // 100 Virg
4864, 382, // 20 Boot
4910, -41, // 105 Virg
4984, 383, // 29 Boot
4986, 322, // 30 Boot
4994, -119, // 107 Virg
5009, 396, // 35 Boot
5013, 53, // 109 Virg
5045, 444, // 37 Boot
5074, -90, // 16 Libr
5108, 57, // 110 Virg
5157, -442, // 24 Libr
5283, -221, // 37 Libr
5290, -329, // 38 Libr
5291, 247, // 13 Serp
5326, -440, // 43 Libr
5331, 455, // 21 Serp
5357, 175, // 27 Serp
5372, 420, // 35 Serp
5381, 109, // 37 Serp
5387, 484, // 38 Serp
5394, -374, // 46 Libr
5415, 364, // 41 Serp
5419, -318, // 48 Libr
5455, -253, // xi Scor
5467, -464, // 9 Scor
5470, -469, // 10 Scor
5497, -437, // 14 Scor
5499, -223, // 15 Scor
5558, 29, // 50 Serp
5561, 441, // 20 Herc
5565, -451, // 4 Ophi
5580, 325, // 24 Herc
5582, -415, // 7 Ophi
5589, -186, // 3 Ophi
5606, -373, // 8 Ophi
5609, 50, // 10 Ophi
5610, -484, // 9 Ophi
5620, 266, // 29 Herc
5713, -241, // 20 Ophi
5742, 235, // 25 Ophi
5763, 217, // 27 Ophi
5807, 293, // 60 Herc
5868, -8, // 41 Ophi
5888, -478, // 40 Ophi
5889, -290, // 53 Serp
5924, -114, //
5925, 96, // 49 Ophi
5987, -183, // 57 Ophi
6006, -292, // 56 Serp
6016, -492, // 58 Ophi
6117, -84, // 57 Serp
6117, 99, // 66 Ophi
6119, 381, // 93 Herc
6119, 67, // 67 Ophi
6125, 30, // 68 Ophi
6146, 57, // 70 Ophi
6158, 198, // 71 Ophi
6170, 473, // 102 Herc
6188, -480, // 13 Sgtr
6234, 76, // 74 Ophi
6235, 499, // 106 Herc
6247, -204, // xi Scut
6254, -469, // 21 Sgtr
6255, 494, // 109 Herc
6278, -333, // ga Scut
6313, -189, // al Scut
6379, 465, // 110 Herc
6382, -110, // be Scut
6386, 411, // 111 Herc
6436, 93, // 63 Serp
6457, 340, // 13 Aqil
6465, -134, // 12 Aqil
6478, -498, // 39 Sgtr
6553, 483, // 1 Vulp
6576, -410, // 44 Sgtr
6576, -368, // 46 Sgtr
6607, 3, // 32 Aqil
6651, 163, // 38 Aqil
6657, 445, // 9 Vulp
6665, -35, // 41 Aqil
6688, 405, // 5 Sgte
6693, 393, // 6 Sgte
6730, 416, // 7 Sgte
6739, 430, // 8 Sgte
6755, 17, // 55 Aqil
6766, 187, // 59 Aqil
6772, 140, // 60 Aqil
6882, 339, // 67 Aqil
6896, -292, // 5 Capr
6898, -292, // 6 Capr
6913, -297, // 8 Capr
6958, -413, // 11 Capr
6988, 250, // 2 Dlph
7001, 326, // 4 Dlph
7015, -33, // 71 Aqil
7020, 475, // 29 Vulp
7026, 354, // 9 Dlph
7047, 335, // 11 Dlph
7066, 359, // 12 Dlph
7067, -225, // 2 Aqar
7068, -123, // 3 Aqar
7096, -213, // 6 Aqar
7161, -461, // 22 Capr
7170, -401, // 23 Capr
7192, -268, // 13 Aqar
7199, 222, // 5 Equl
7223, 219, // 7 Equl
7230, 110, // 8 Equl
7263, -393, // 32 Capr
7267, 441, // 1 Pegs
7299, -506, // 36 Capr
7347, -453, // 39 Capr
7353, -189, // 23 Aqar
7365, -390, // 40 Capr
7379, -440, // 43 Capr
7394, 384, // 9 Pegs
7499, -60, // 31 Aqar
7513, 104, // 22 Pegs
7515, -327, // 33 Aqar
7575, -189, // 43 Aqar
7603, -43, // 48 Aqar
7604, 266, // 31 Pegs
7624, 20, // 52 Aqar
7639, 96, // 35 Pegs
7654, -255, // 57 Aqar
7681, -14, // 62 Aqar
7727, -440, // 66 Aqar
7747, 266, // 46 Pegs
7761, -321, // 71 Aqar
7779, -185, // 73 Aqar
7795, 189, // 50 Pegs
7844, 75, // 4 Pisc
7862, 202, // 55 Pegs
7874, -494, // 88 Aqar
7903, -150, // 90 Aqar
7911, -219, // 91 Aqar
7919, 62, // 6 Pisc
7923, -222, // 93 Aqar
7952, -470, // 98 Aqar
7969, -482, // 99 Aqar
7975, 16, // 8 Pisc
7981, 133, // 10 Pisc
7988, 278, // 70 Pegs
8010, -489, // 101 Aqar
8049, 116, // 17 Pisc
8059, -418, // 104 Aqar
8061, 28, // 18 Pisc
8064, -344, // 105 Aqar
8159, 144, // 28 Pisc
8174, -149, // 30 Pisc
8188, -407 // 2 Ceti
]
};
m1 = new Magnitude('1j');
m2 = new Magnitude('2j');
m3 = new Magnitude('3j');
m4 = new Magnitude('4j');
// return API
return {
'update': update,
'setOption': setOption,
'reset': reset
};
})();
/* ====== implementation specific glue ====== */
// plot int co-ordinates, normalized (x: 0..1024, y: 0..1024, origin top-left) to CRT
function plot(x, y, b) {
x = (COORS_MAX + x) % SCREENWIDTH;
y = (SCREENWIDTH - (COORS_MAX + y)) % SCREENWIDTH;
if (x < 0) x += SCREENWIDTH;
if (y < 0) y += SCREENWIDTH;
CRT.plot(x, y, b);
/* alternatively plot to integer coors:
CRT.plot(x|0, y|0, b);
*/
}
// optional UI notifications
function displayScores() {
if ((typeof SpacewarUI === 'object' || typeof SpacewarUI === 'function')
&& typeof SpacewarUI.showScores === 'function') {
SpacewarUI.showScores(score1, score2);
}
}
function haltSignal() {
if ((typeof SpacewarUI === 'object' || typeof SpacewarUI === 'function')
&& typeof SpacewarUI.halted === 'function') {
SpacewarUI.halted();
}
}
// notify any external UI to check gamepads (on the entry of each frame).
// the UI is expected to set any readings via Spacewar.setControls().
function readGamepads() {
if ((typeof SpacewarUI === 'object' || typeof SpacewarUI === 'function')
&& typeof SpacewarUI.readGamepads === 'function') {
SpacewarUI.readGamepads();
}
}
// some sane setters for external use
/*
setter for sense switch settings
e.g., Spacewar.setOption( 'ANGULARMOMENTUM', true )
or Spacewar.setOption( 'SenseSwitch 1', true )
*/
function setOption(key, value) {
var k = String(key).toUpperCase().replace(/[^A-Z1-6]/g, '');
switch (k) {
case 'SENSESWITCH1': Options.ANGULARMOMENTUM = Boolean(value); break;
case 'SENSESWITCH2': Options.LOWGRAVITY = Boolean(value); break;
case 'SENSESWITCH3': Options.SINGLESHOTS = Boolean(value); break;
case 'SENSESWITCH4': Options.NOBACKGROUND = Boolean(value); break;
case 'SENSESWITCH5': Options.SUNKILLS = Boolean(value); break;
case 'SENSESWITCH6': Options.SUNOFF = Boolean(value); break;
case 'FPS': setFPS(value); break;
default:
if (typeof Options[k] !== 'undefined') Options[k] = Boolean(value);
break;
}
}
/*
getter for Options
*/
function getOption(key) {
var k = String(key).toUpperCase().replace(/[^A-Z1-6]/g, '');
switch (k) {
case 'SENSESWITCH1': return Options.ANGULARMOMENTUM ;
case 'SENSESWITCH2': return Options.LOWGRAVITY;
case 'SENSESWITCH3': return Options.SINGLESHOTS;
case 'SENSESWITCH4': return Options.NOBACKGROUND;
case 'SENSESWITCH5': return Options.SUNKILLS;
case 'SENSESWITCH6': return Options.SUNOFF;
case 'FPS': return FPS;
default: return Options[k];
}
}
/*
setter for spaceship control input
e.g., Spacewar.setControls( 'SPACESHIP1', 'FIRE', true )
or Spacewar.setControls( 0, 'FIRE', true )
or Spacewar.setControls( Spacewar.Controls.SPACESHIP1, Spacewar.Controls.FIRE, true )
reset state by Spacewar.setControls( 'SPACESHIP1', 'RESET' )
or Spacewar.setControls( 'SPACESHIP1', 'ALL', false )
*/
function setControls(spaceship, key, value) {
var s, b, obj;
if (!mtb.length) return; // not started, yet: ignore
// sanitize input
switch (typeof spaceship) {
case 'number':
s = spaceship | 0;
if (s < 0 || s > 1) return;
break;
case 'string':
spaceship = spaceship.toUpperCase();
if (spaceship === 'SPACESHIP1' || spaceship === 'SPACESHIP2') {
s = Controls[spaceship];
break;
}
else {
return;
}
default: return;
}
switch (typeof key) {
case 'number':
b = key | 0;
if (!Controls.legalInputs[b]) return;
break;
case 'string':
key = key.toUpperCase();
if (Controls[key] !== 'undefined') {
b = Controls[key];
break;
}
else {
return;
}
default: return;
}
// finally, manipulate the bit-vector in property 'ctrl'
obj = mtb[s];
obj.ctrl = ( (value)? obj.ctrl | b : obj.ctrl & (~b) ) & Controls.ALL;
}
/*
setter for testword
*/
function setTestword(n) {
var tw = parseInt(String(n), 10);
if (!isNaN(tw)) testword = Math.abs(tw) & 0x3FFFF;
}
/*
getter for testword
*/
function getTestword() {
return testword;
}
/*
setter for FPS, called by "setOption('FPS', <value>)" internally
*/
function setFPS(n) {
var fps = parseFloat(String(n));
if (!isNaN(fps) && fps > 0 && fps <= 100) {
Options.FPS = fps;
if (timer) {
clearInterval(timer);
timer = setInterval(frame, Math.floor(1000/fps));
}
}
}
/*
getter for FPS
*/
function getFPS() {
return FPS;
}
/*
getter for scores, returns array [<score-ss1>, <score-ss2>]
*/
function getScores() {
return [score1, score2];
}
/*
clear/reset scores
*/
function resetScores() {
score1 = score2 = 0;
displayScores();
}
/*
halt execution
*/
function halt() {
if (timer) halted = true;
}
/*
resume from halt
*/
function resume() {
halted = false;
}
// return API object
return {
'setOption': setOption,
'Controls': Controls,
'setControls': setControls,
'setTestword': setTestword,
'getTestword': getTestword,
'setFPS': setFPS,
'getFPS': getFPS,
'getScores': getScores,
'resetScores': resetScores,
'halt': halt,
'resume': resume,
'run': run,
'EPsetOption': ExpensivePlanetarium.setOption
};
})();