Episode 9: Scores, Just in Time
The end is nigh! — Not of the world, hopefully, but of October — and that is, of Retrochallenge. So it's only appropriate that we should catch the first glimpses of the finishing line. In order to get there, we have to figure out first, how to output figures onto the scope.
So we're going to implement the scores. On the level of just keeping scores this is a task rathe mundane: Just add two variables, one for the score of the player (rocket) and another one for the saucers, clear them at the start of a game and advance them on each hit.
As it comes to displaying the scores, things are a bit different: The PDP-1 doesn't feature any support for character output onto the scope by default (while there is, in fact, an optional symbol generator, Type 33, at just US$ 4,000 in 1963 — just two brand-new cars!). We'll have to do this on our own, programwise, — and this is a time consuming task! Thus, we're chosing to implement an outline compiler to produce procedural object code for drawing any of the characters as fast as possible, so that we may go flicker-free. We're going to do a simple JIT compiler to be run at the start of the program, just as Dan Edwards did it for the outlines of Spacewar!'s spaceships around the turn of the year 1961/62.
(Note: If we were targeting the CHM machine only, we could dispense with the effort, since this one has, in fact, a high-speed symbol generator. But we want to keep this program as general as possible and, by the way, our humble emulator lacks a symbol generator, as well.)
First, we need some characters to draw, and we will need the numbers 0 to 9 and the characters A to F, since Computer Space features a unique one-digit hex display for the scores. While the arcade machine renders its numerals in a 7-segement display style, we can't do this, if we want to stick with the spirit of the period. 7-segment displays where widely unknown and LEDs weren't commonly available at all before 1968. This was the era of Nixie tubes and their beautiful glowing numbers — and we really need something more appropriate to match the PDP-1 and the appearance of its scope.
We go with what seems to be the standard definition, which may be found in a subroutine published by DEC in 1961. (See PDP-1_Subroutines_1961.pdf at bistavers.org [at the end of document], also, as of 1964 for the symbol generator, CHM, Catalog No. 102636251.)
(Note: Our PDP-1 emulator renders these definitions, as well, in JavaScript, for the purpose of its splash screen. If you press the tiny black button at the bottom right of the emulator's scope, just where the outlet for the cable of the light pen would be on the real machine, you'll get a service screen including the entire character dump.)
And this is what these characters look like, as far as required by our program:
The characters are defined for a 7 rows × 5 dots grid of arbitrary scale. We're not going to render the dots at uniform distances for both vertical and horizontal dimensions, as shown here, but compressed, in an aspect ratio favoring the vertical extent.
While the authentic character data is organized columnwise, we prefer to have it in rows (we'll see late, why). Therefor, we generate our own, two 18-bit words for each character, lining up 5 bits for each rows, starting at the left and chaining rows in top-down order. For this, we write a little script to generate the data words in the browser (why not?), to be seen here, ICSS Character Data Generator.
With this in place, we have ultimately arrived at the end of any further procrastination and have to address the outline compiler.
We're going to scan the code words (character data) left to right, keep track of the x-index of the bit currently scanned and of the x-coordinate of the last display command compiled, add any offsets where needed, and compile appropriate display commands. As we're moving along the x-axis, we're going mostly just by modifying the x-location in the accumulator by additions and subtractions. Just at the end of a row, we have to swap the y-coordinate in and out to advance this one. We optimize our compiler a little bit, by advancing bidirectional, but just for the first bit scanned in a row, since we do not have any look-ahead to do much more. And of course, we have to keep track of our bit position in the data, in order to advance to the second code word at the right time.
And this is how we do it:
- Get the start address for the compiled object code.
- Get the index of the character and derive the location of the first code word.
- Load the code word into IO.
- Reset counters for y-position and last x to be displayed.
- Compile initial instructions to load x and y of start position (in AC and IO respectively).
- Now, do a row:
- Reset x-counter.
- Check sign bit (leftmost bit) in IO.
- If set,
- Calculate offset to last display position.
- Compile an appropriate addition or subtraction.
- Update last drawing position (x).
- Compile "
ioh
" to wait for the display (completion pulse) — Skipped for first dot of a character. - Compile the command to display a dot and request a completion pulse.
- Rotate IO to set up next bit in sign.
- Have we reached the end of a word? Get next code word.
- Increment x-counter.
- Have we reached the end of a row?
- If not so, redo at the instruction following to the reset of x-counter.
- Increment row:
- Increment y-count. Have we done 7 rows already?
- If so, jump to the coda.
- Compile a swap.
- Compile an instruction to add a row offset.
- Compile another swap. (Now x is back in AC and y back in IO for the compiled code.)
- Jump to start of a row.
- Coda:
- Compile another "
ioh
" to fetch and clear the last completion pulse. - Compile a jump to exit location.
- Compile another "
- Return with the address of next location after compiled object code in AC (= start of next outline code).
Now we may run the outline compiler for each of the characters and store the respective start addresses in a table. (The compiled object code will go after the code of the program and the space reserved for its variables.) Also, we produce a little calling routine, to be called by the "jda
" instruction, passing the digit (character index) in AC, which selects the respective routine from the table and calls it. This is also, where our compiled outline routine returns to.
The real beauty in this is that, while we would have to frequently store states and intermediate results for an interpreter with just two CPU registers to go with, the compiler and the object code compiled by it can go nicely along by just dealing with the accumulator. (IO is used for the code word only in the compiler, and for the y-component in the object code.) As a result, the compiled code is running as fast as the display allows (10 CPU cyles delay between display commands).
Our little optimization strategy is working great for vertical lines, like the vertical stroke in a "1", but does little for other figures, like a "3", because of the lack of any look-ahead for further directional optimization. Here's a small example of the code produced by the compiler:
/* ICSS character outline compiler * /compiled code for characters '1' and '3' /unit offset y 02564 010000 010000 / cgy (16 screen locations) /unit offsets x (2..4 generated by program) 02565 006000 006000 / cgx x 1 (12 screen locations) 02566 014000 014000 / x 2 02567 022000 022000 / x 3 02570 030000 030000 / x 4 / ' * ' / ' ** ' / ' * ' / ' * ' / ' * ' / ' * ' / ' * ' /generated outline code for character '1' /addr instr disass symbolic notes 03421 222750 lio 2750 / lio cy start y in io 03422 202747 lac 2747 / lac cx start x in ac 03423 402566 add 2566 / add 2566 03424 724107 iot 4107 / dpy-i 4100 display a dot 03425 673777 rcr 777 / rcr 9s swap 03426 673777 rcr 777 / rcr 9s 03427 422564 sub 2564 / sub cgy next row, inc. y 03430 673777 rcr 777 / rcr 9s 03431 673777 rcr 777 / rcr 9s 03432 422565 sub 2565 / sub cgx - 1 unit 03433 730000 ioh / ioh wait for display 03434 724107 iot 4107 / dpy-i 4100 display a dot 03435 402565 add 2565 / add cgx + 1 unit 03436 730000 ioh / ioh 03437 724107 iot 4107 / dpy-i 4100 display a dot 03440 673777 rcr 777 / rcr 9s swap 03441 673777 rcr 777 / rcr 9s 03442 422564 sub 2564 / sub cgy next row 03443 673777 rcr 777 / rcr 9s swap 03444 673777 rcr 777 / rcr 9s 03445 730000 ioh / ioh 03446 724107 iot 4107 / dpy-i 4100 display a dot 03447 673777 rcr 777 / rcr 9s swap 03450 673777 rcr 777 / rcr 9s 03451 422564 sub 2564 / sub cgy next row 03452 673777 rcr 777 / rcr 9s 03453 673777 rcr 777 / rcr 9s 03454 730000 ioh / ioh 03455 724107 iot 4107 / dpy-i 4100 display a dot 03456 673777 rcr 777 / rcr 9s swap 03457 673777 rcr 777 / rcr 9s 03460 422564 sub 2564 / sub cgy next row 03461 673777 rcr 777 / rcr 9s swap 03462 673777 rcr 777 / rcr 9s 03463 730000 ioh / ioh 03464 724107 iot 4107 / dpy-i 4100 display a dot 03465 673777 rcr 777 / rcr 9s swap 03466 673777 rcr 777 / rcr 9s 03467 422564 sub 2564 / sub cgy next row 03470 673777 rcr 777 / rcr 9s swap 03471 673777 rcr 777 / rcr 9s 03472 730000 ioh / ioh 03473 724107 iot 4107 / dpy-i 4100 display a dot 03474 673777 rcr 777 / rcr 9s swap 03475 673777 rcr 777 / rcr 9s 03476 422564 sub 2564 / sub cgy next row 03477 673777 rcr 777 / rcr 9s swap 03500 673777 rcr 777 / rcr 9s swap 03501 730000 ioh / ioh 03502 724107 iot 4107 / dpy-i 4100 display a dot 03503 730000 ioh / ioh 03504 602757 jmp 2757 / jmp cdx exit / ' *** ' / '* *' / ' *' / ' ** ' / ' *' / '* *' / ' *** ' /generated outline code for character '3' /addr instr disass symbolic notes 03621 222750 lio 2750 / lio cy start y in io 03622 202747 lac 2747 / lac cx start x in ac 03623 402565 add 2565 / add cgx + 1 unit 03624 724107 iot 4107 / dpy-i 4100 display a dot 03625 402565 add 2565 / add cgx + 1 unit 03626 730000 ioh / ioh wait for display 03627 724107 iot 4107 / dpy-i 4100 display next dot 03630 402565 add 2565 / add cgx + 1 unit 03631 730000 ioh / ioh 03632 724107 iot 4107 / dpy-i 4100 03633 673777 rcr 777 / rcr 9s swap 03634 673777 rcr 777 / rcr 9s 03635 422564 sub 2564 / sub cgy next row 03636 673777 rcr 777 / rcr 9s swap 03637 673777 rcr 777 / rcr 9s 03640 422567 sub 2567 / sub cgx + 2 - 3 units 03641 730000 ioh / ioh 03642 724107 iot 4107 / dpy-i 4100 03643 402570 add 2570 / add cgx + 3 + 4 units 03644 730000 ioh / ioh 03645 724107 iot 4107 / dpy-i 4100 03646 673777 rcr 777 / rcr 9s swap 03647 673777 rcr 777 / rcr 9s 03650 422564 sub 2564 / sub cgy next row 03651 673777 rcr 777 / rcr 9s swap 03652 673777 rcr 777 / rcr 9s 03653 730000 ioh / ioh 03654 724107 iot 4107 / dpy-i 4100 03655 673777 rcr 777 / rcr 9s swap 03656 673777 rcr 777 / rcr 9s 03657 422564 sub 2564 / sub cgy 03660 673777 rcr 777 / rcr 9s swap 03661 673777 rcr 777 / rcr 9s 03662 422566 sub 2566 / sub cgx + 1 - 2 units 03663 730000 ioh / ioh 03664 724107 iot 4107 / dpy-i 4100 03665 402565 add 2565 / add cgx + 1 unit 03666 730000 ioh / ioh 03667 724107 iot 4107 / dpy-i 4100 03670 673777 rcr 777 / rcr 9s swap 03671 673777 rcr 777 / rcr 9s 03672 422564 sub 2564 / sub cgy next row 03673 673777 rcr 777 / rcr 9s swap 03674 673777 rcr 777 / rcr 9s 03675 402565 add 2565 / add cgx + 1 unit 03676 730000 ioh / ioh 03677 724107 iot 4107 / dpy-i 4100 03700 673777 rcr 777 / rcr 9s swap 03701 673777 rcr 777 / rcr 9s 03702 422564 sub 2564 / sub cgy next row 03703 673777 rcr 777 / rcr 9s swap 03704 673777 rcr 777 / rcr 9s 03705 422570 sub 2570 / sub cgx + 3 - 4 units 03706 730000 ioh / ioh 03707 724107 iot 4107 / dpy-i 4100 03710 402570 add 2570 / add cgx + 3 + 4 units 03711 730000 ioh / ioh 03712 724107 iot 4107 / dpy-i 4100 03713 673777 rcr 777 / rcr 9s swap 03714 673777 rcr 777 / rcr 9s 03715 422564 sub 2564 / sub cgy next row 03716 673777 rcr 777 / rcr 9s swap 03717 673777 rcr 777 / rcr 9s 03720 422567 sub 2567 / sub cgx + 2 - 3 units 03721 730000 ioh / ioh 03722 724107 iot 4107 / dpy-i 4100 03723 402565 add 2565 / add cgx + 1 unit 03724 730000 ioh / ioh 03725 724107 iot 4107 / dpy-i 4100 03726 402565 add 2565 / add cgx + 1 unit 03727 730000 ioh / ioh 03730 724107 iot 4107 / dpy-i 4100 03731 730000 ioh / ioh 03732 602757 jmp 2757 / jmp cdx exit
With this installed, our program is still running fast, maybe just a tick too fast, even with some throttle left. Quite promising and pleasing, as well.
Implementation — As in Real Life
Fear and Loathing (and Despair) at Las Emulatores
As mentioned earlier, the in-browser emulator lacks any debugging facilities. There isn't even a way to log to any console from inside a program. For the purpose of testing a JIT compiler, not the best of preconditions. And the devil never sleeps…
As could be expected, I'm seeing nothing on the screen on the first run. There isn't even an illfated jump causing an infinite loop (which would be eventually detected by the emulator). — Hum.
We detect an error and finally see some outline, but it is skewed and distorted. — Hum?!
Staring at the code reveals little of errors, so we actually need some way to inspect the results of our attempts of compiling procedural outline code. — We produce a test version of the emulator by inserting a few lines to export a memory dump on a key press. Also, there's a rudementary disassembler (see the edited output above), we have produced once to peek into some ancient object tapes (of course, related to Spacewar!). So, dump the memory, copy'n'paste it into a source file and run the disassembly.
Lets stare at the code again. — Hum… — Not much to see there, all looks fine. We insert test statements, change parameters, reassemble the code, run it, dump the memory, copy'n'paste, disassemble, stare — rinse & repeat.
This uses actually more than an hour without results. First, we even get some entirely senseless dissassembly, until we detect that our code dump somehow forgot to add zeros in front of any numbers (since it's all octal). Nevertheless, when we eventually see the compiled code as-is, there's nothing to see, as far as it were for any errors.
Eventually, we tinker with the character dimensions, and there's some irritation. The horizontal offsets just don't match. We have them in a static table, 4 numbers in octal, multiples of the base unit. Should be easy, but it isn't. We use a calculator to add them correctly. We wrap our head around octal numbers, but it still doesn't work out. — At last, we implement the multiples of the grid unit to be generated by the program … and it finally works.
I still don't know, where this error in the offset values may have been and how it may have occured, since accumulating numbers should be easy, shouldn't it? Even by the use of a calculator and repetitively hitting the sum key. — So the code devil had another sleepless hour …
Friendly Outlook
In order to not to close in thus a gloomy temper, we really have to announce that we're seeing the finishing line! We reorganized a few snippets by relocating the motion tables to the front of the code and tinkered a bit with the AI (essentially by changing from noise in the aiming epsilon to noise in the assumed targed position, as already announced last episode). And the AI may be even a bit too good by now. We'll see.
All that's left to do, besides adjusting a few parameters, is to implement an attract mode and time keeping for the count-up on the duration of the game. And, we'll have to adjust the pace and find out, how much time we're actually spending in the major routines in order to pace the game and its frame rate properly. (And here, life will be actually easier with the emulator, as we'll simply let it dump the numbers of cycles spent between visiting any of two memory locations.)
And this is what our game looks like after affixing an emulated note to the emulated scope as a legend to the displayed numerals produced by the program emulated in the virtual PDP-1, simulating an ancient, while younger, coin-op. machine, which the real PDP-1 and Spacewar! had once inspired:
▶ Experience the code live: www.masswerk.at/icss/.
And here are the respective labels (at the side of the screen) at/on/in the original Computer Space machine:
Still To Do
- Time keeping and count-up (translate frames to something like seconds).
- Attract mode between games.
- Fine tuning.
And here's our growing code, so far:
ironic computer space 0.4 nl 2016-10-28 mul=mus /instruction code for multiplication div=dis /instruction code for division ioh=iot i /wait for completion pulse (CRT display) szm=sza sma-szf /skip on ac zero or minus spq=szm i /skip on ac plus and not zero define initialize A,B law B dap A term define index A,B,C idx A sas B jmp C term define swap rcl 9s rcl 9s term define load A,B lio (B dio A term define setup A,B law i B dac A term define count A,B isp A jmp B term /macros specific to the program define scale A,B,C lac A sar B dac C term define random lac ran rar 1s xor (355671 add (355671 dac ran term / Computer Space / Original arcade game by Nolan Bushnell and Ted Dabney, / Syzygy Engineering / Nutting Associates, 1971. / 'A simulated space battle that pits / computer-guided saucers against / a rocket ship that you control.' / sense switch options / SSW 1 ... parallax effect / off - stars roll continuously to the left. / on - background stars move raltively to rocket ship. / SSW 2 ... parallax effect strength (together with SSW 1) / off - subtle effect. / on - stronger effect. / SSW 3 ... torpedoes, single shots (like spacewar!) / off - 'continuous fire' action. / on - trigger blocks and waits for new press. / SSW 4 ... torpedoes, steering / off - steering applies only, while the ship is turning. / on - torpedoes eventually follow the turn of the ship. / SSW 5 ... saucer motion / off - diagonals are horizontally stretched. / (conforms more to the overall impression of CS) / on - geometrical diagonals. / SSW 6 ... saucer piloting, which saucer is shooting? / off - always the same one (as in original CS). / on - random select. 3/ jmp sbf / ignore seq. break jmp a0 / start addr, use control boxes jmp a1 / alt. start addr, use testword controls /game parameters raa, 6, 1200 / rocket angular acceleration rvl, 7, sar 8s / scaling rocket velocity ras, 10, law i 6 / rocket acceleration sensitivity (skip) rad, 11, law i 4 / rocket acceleration damping (averaging) trv, 12, sar 7s / scaling torpedo velocity tlf, 13, law i 250 / torpedo life rsd, 14, 60 / rocket reset delay (frames) etl, 15, law i 240 / enemy torpedo life etd, 16, law i 30 / enemy torpedo cooling (frames) etf, 17, law i 100 / enemy first torpedo cooling (after score) ela, 20, sal 6s / scaling enemey torp look-ahead ete, 21, 14000 / epsilon for enemy aiming etn, 22, sal 3s / scaling noise for enemy torpedo aim ran, 23, 0 / random number /saucer movement table (dy, dx) / diagonals proper ut1, 600 0 -600 0 0 600 0 -600 400 400 -400 400 400 -400 -400 -400 / horizontally streched diagonals, default (dy, dx) ut0, 600 0 -600 0 0 600 0 -600 400 600 -400 600 400 -600 -400 -600 /saucer torpedo movement table (dy, dx) emt, 2400 0 -2400 0 0 2400 0 -2400 1600 1600 -1600 1600 1600 -1600 -1600 -1600 /routine to flush sequence breaks, if they occur. sbf, tyi lio 2 lac 0 lsm jmp i 1 /sine-cosine subroutine - Adams associates /calling sequence= number in AC, jda sin or jdacos. /argument is between +/- +2 pi, with binary point to right of bit 3. /answer has binary point to right of bit 0. Time = 2.35-? ms. /changed for auto-multiply , ddp 1/19/63 cos, 0 dap csx lac (62210 add cos dac sin jmp .+4 sin, 0 dap csx lac sin spa si1, add (311040 sub (62210 sma jmp si2 add (62210 si3, ral 2s mul (242763 dac sin mul sin dac cos mul (756103 add (121312 mul cos add (532511 mul cos add (144417 mul sin scl 3s dac cos xor sin sma jmp csx-1 lac (377777 lio sin spi cma jmp csx lac cos csx, jmp . si2, cma add (62210 sma jmp si3 add (62210 spa jmp .+3 sub (62210 jmp si3 sub (62210 jmp si1 /subroutines for background display nos=77 /number of stars /table of stars coors (nos words, 9 bits x and 9 bits y) bst, . nos/ /setup (nos random coors starting at bst) bsi, dap bsx / deposit return address init bsc, bst / deposit first addr of bst in bsc bsl, random / get a new random number bsc, dac . / store it in current loc of st index bsc, (dac bst+nos, bsl / increment bsl, repeat at bgl for nos times bsx, jmp . / return /display background stars (displays every 2nd frame only) bg, dap bgx / deposit return address lac \frc / check frame conter and (1 sza / skip every second frame jmp bgi / jump to star display szs i 10 / sense switch 1 for parallax effect jmp bgd lac \rdx szs i 20 / sense switch 2 for stronger effect jmp . 3 sar 1s add \rdx sar 3s cma add bgh dac bgh lac \rdy szs i 20 jmp . 3 sar 1s add \rdy sar 3s cma add bgv dac bgv jmp bgx / return bgd, lac \frc / advance x offset slowly and (177 sza jmp bgx law i 400 add bgh dac bgh jmp bgx / return bgi, init bgl, bst / init bgl to first addr of stars table bgl, lac . / get coors of next star (x/y) cli / clear io scr 9s / shift low 9 bits in high 9 bits of io (x) sal 9s / move remaining 9 bits in ac in high part (y) add bgv / add vertical offset swap / swap contents of ac and io add bgh / add horizontal offset dpy-i / display a dot at coors in ac (x) and io (y) index bgl, (lac bst+nos, bgl / repeat the loop at bgl nos times bgx, jmp . / return bgh, 0 bgv, 0 /here from start a0, law rcb /configure to read control boxes (sa 4) dap rcw jmp a2 a1, law rtw /configure to read testword (sa 5) dap rcw /start a new run a2, jsp ci jsp bsi / initial setup of bg-stars dzm bgh / reset offsets of stars to zero dzm bgv /rocket setup ar, lac (240000 dac \rth / rotational angle lac (-200000 dac \rpx / pos x cma dac \rpy / pos y law 300 dac \rdx law i 500 dac \rdy dzm \rac / acceleration skip counter dzm \rxc / reset center offset dzm \ryc law 1 dac \rks /status dzm \sc1 /score dzm \trs /torpedo status counter dzm \cwo /old control word /saucer setup au, lac (200000 dac \upy / pos x lac (140000 dac \upx / pos y dzm \udc / animation skip counter law 1 dac \udd / animation direction dzm \umo / direction code law 600 dac \udy / delta y dzm \udx / delta x law i 100 dac \ufc / duration of movement law 1 dac \uft / speed of center animation (1,3) law 1 dac \ufs / state counter dzm \sc2 dzm \ets / torpedo state jsp etr / get torpedo cooling (\etc) /main loop fr0, load \ict, -2040 / initial instruction budget (delay) idx \frc / increment frame counter clf 1 / flag 1 indicates active player's ship clf 2 clf 3 jsp bg / display background jsp rkt / rocket routine jsp ufo lac \trs sza jsp trp / torpedo routine lac \ets sza jsp etm / enemy torpedo routine jsp df / check collisions lac \cw dac \cwo jsp scd /display scores count \ict, . / use up rest of time of main loop jmp fr0 / next frame /score display scd, dap scx lac (330000 dac cx lac (144000 dac cy lac \sc1 and (17 jda cdp lac (30000 dac cy lac \sc2 and (17 jda cdp lac (-64000 dac cy cla jda cdp lac (260000 dac cx cla jda cdp scx, jmp . /collision / hit detection df, clf 5 dap dfx lac \ufs /check saucer state spq /skip on positive jmp dfx lac \rks /check rocket state spq jmp dfx lac \rpx /get rotational center of rocket sub \rxc dac \px lac \rpy add \ryc dac \py law \px /set up rocket for comparison dap mx1 law \py dap my1 lac (21000 /set up collision radii (x 34, y 24) in screen locs dac \dxe sar 1s dac \de2 lac (14000 dac \dye law \upx /first saucer dap mx2 law \upy dap my2 jsp dmf /compare them sza /hit? jmp dxr /yes lac \upy /second saucer sub (400000 /vertical offset dac \vpy law \vpy dap my2 jsp dmf /compare them sza /hit? jmp dxr /yes dft, lac \trs /torpedo active? sma jmp dfe lac (12200 /setup hit box (x 21, y 10) dac \dxe sar 1s dac \de2 lac (5000 dac \dfy law \tpx /set up torp as first object dap mx1 law \tpy dap my1 jsp dmf /compare sza jmp dxu /hit, respawn law \upy dap my2 jsp dmf /compare them sza /hit? jmp dxu dfe, lac \ets /enemy torpedo active? sza i jmp dfx lac (11000 dac \dxe dac \dye law 6400 dac \de2 law \px dap mx1 law \py dap my1 law \epx dap mx2 law \epy dap my2 jsp dmf sza i jmp dfx dzm \ets /reset enemy torpedo xct etf /get initial topredo cooling dac \etc /and store it lac stf 5 dxr, law i 100 /set rocket state to collision sub rsd /additional delay before reset dac \rks dzm \rdx dzm \rdy dzm \trs lac \rth dac \th0 dzm \thc law i 10 dac \thd idx \sc2 szf i 5 jsp urs /respawn saucers jmp dfx dxu, dzm \trs /set up saucer explosion, reset torpedoes dzm \ets lac i my2 /set up pos y dac \upy idx \sc1 law i 30 30 dac \ufs dfx, jmp . /return /subroutine to compare object positions (from spacewar, mod n.l.) dmf, dap dmx mx1, lac . /calc if collision mx2, sub . /delta x spa /take abs val cma dac \t1 sub \dxe / < epsilon x ? sma jmp dm0 /no my1, lac . my2, sub . spa cma sub \dye / < epsilon y ? sma jmp dm0 /no add \t1 sub \de2 / < epsilon 2 ? sma jmp dm0 law 1 /return 1 (in ac) for hit jmp dmx dm0, cla /return 0 for miss dmx, jmp . /saucers ufo, dap ufx lac \ufs spq jmp uxe clf 6 lac \upx /update position add \udx dac \upx dac \px lac \upy add \udy dac \upy dac \py jsp sd /display first saucer lac \py sub (400000 /half-screen vertical offset dac \py jsp sd /display second saucer isp \ufc /increment leg counter jmp uf1 /continue lac \umo /new direction spa jmp uza /we're in a 3-steps stop (\umo: -3..-1) sar 3s / 010 set for single step stop sza jmp uz1 random sma jmp uf2 /set up new leg dzm \udx /stop dzm \udy lac ran /what kind of stop will it be? ral 3s spa jmp uz0 /three-steps stop law 10 /single-step stop, keep center animation dac \umo jmp uzb /get delay and continue uz0, law i 3 /first period of three-steps stop dac \umo /reuse \umo as step counter (negative) law 3 dac \uft /set animation to slow (3) uzb, jsp utd /get duration sar 1s dac \ufc jmp eta /continue for torpedo setup uza, cma /3-steps stop, dispatch on -\umo (-3..-1) add (. 3 dap . 2 idx \umo jmp . jmp uz1 jmp uz2 uz3, dzm \udd /3 - stop animation jmp . 2 uz2, jsp ucd /2 - still stopped, new animation direction jsp utd sar 1s dac \ufc jmp eta uz1, stf 6 /1 - start over, but keep animation random jmp . 2 uf2, clf 6 /set up for a new leg (flag 6: keep anim. dir.) and (7 dac \umo /new motion code (0..7) sal 1s /read corresponding dx/dy form movement table szs i 50 /sense switch 5 to select table (ut1 or ut0) jmp . 3 add (ut1 /setup reference location for dy in \t1 jmp . 2 add (ut0 /select other table dac \t1 lac i \t1 /load dy indirectly from addr. in \t1 dac \udy idx \t1 /increment addr. in \t1 lac i \t1 /load dx indirectly from addr. in \t1 dac \udx szf 6 i /skip next on flag 6 jsp ucd /set new direction for animation jsp utd /get delay dac \ufc uf1, lac \frc /increment center animation and \uft sza jmp eta lac \udc /udc = 0..5 sub \udd dac \udc sma jmp . 4 law 5 dac \udc jmp eta sad (6 dzm \udc jmp eta ufi, lac \ict /update instruction count add (1200 dac \ict ufx, jmp . ucd, dap ucx /subroutine to set center animation lio ran ril 1s law 1 spi cma dac \udd lac ran /animation speed, favor faster rar 9s and (3 sza jmp . 3 law 1 jmp . 2 law 3 dac \uft ucx, jmp . utd, dap utx /subroutine to get random delay (leg length) random sar 9s sar 3s sub (270 utx, jmp . urs, dap urx /subroutine to respawn the saucers lac ran /random offset x to player's rocket rar 9s sar 2s dac \upx sar 1s add \upx add (400000 add \rpx dac \upx random /random pos y dac \upy jsp ucd /set up center animation dac \udd law i 40 /set up delay before next leg dac \ufc law i 1 /stopped (end of 3-steps stop) dac \umo jsp etr /reset torpedo, get cooling law 1 /set state to active dac \ufs urx, jmp . /saucer explosion (similar to spacewar) / jump here from inside ufo uxe, law uxs /set up instr at uxs to be xct-ed in ux1 dap ux1 lac \ufs add (30 /done with explosion, 030 frames setup delay sma jmp ux3 sal 1s dac \uxc /repeat for 030 particles add (50 /first and last frames small explosion spa jmp ux0 sub (30 sma idx ux1 /increment addr in ux1 for bigger explosion ux0, random /start of loop, get random number and (6s /constrain to lowest 6 bits (as no of shifts) ior (scl /assemble a shift instruction for these dac ux2 /insert it at ux2 random /new random number cli scr 9s /split it into lower parts of ac and io sir 9s ux1, xct . /apply scaling (size) ux2, opr /apply shift for random spread add \upy /add center pos x/y swap add \upx dpy -i 100 /display a dot isp \uxc /redo \uxc times jmp ux0 ux3, isp \ufs /increment status counter jmp ufi jsp urs /last iteration, respawn saucers jmp ufi uxs, scl 1s /scaling for small explosion scl 2s / and bigger one, to be xct-ed at ux1 /saucer torpedo ai (setup) / here from inside ufo eta, lac \ets /torpedo already active? spa jmp ufi szf i 1 /rocket active? jmp ufi lac \ufs /saucers active? spq jmp ufi isp \etc /count up cooling jmp ufi dzm \etc lac \upx /position of tubes add (10400 dac \epx lac \upy sub (3400 dac \epy random /get random offset for targeting cli scr 9s /split it into lower parts of ac and io sir 9s xct etn /scale it dac \exn /store noise x swap /same for y xct etn dac \eyn szs i 60 /sense switch 6 to select saucers at random jmp et1 / original shoots always from same saucer random sma /which saucer jmp et1 lac \epy add (400000 dac \epy et1, / random / sar 9s / add ete / spa / cma lac ete dac \eps lac \rdx xct ela /look-ahead add \rpx sub \rxc add \exn sub \epx dac \dx /delta x spa cma dac \adx /abs delta x lac \rdy /same for y xct ela add \rpy add \ryc add \eyn sub \epy dac \dy spa cma dac \ady sub \eps spa jmp eah lac \adx sub \eps spa jmp eav lac \adx sub \ady spa cma sub eps sma jmp ufi ead, law 4 lio \dy spi law 5 lio \dx spi add (2 jmp et3 eah, law 2 lio \dx spi law 3 jmp et3 eav, cla lio \dy spi law 1 et3, sal 1s /read corresponding dx/dy form movement table add (emt /setup reference location for dy in \t1 dac \t1 lac i \t1 /load dy indirectly from addr. in \t1 dac \edy idx \t1 /increment addr. in \t1 lac i \t1 /load dx indirectly from addr. in \t1 dac \edx xct etl dac \ets eti, /lac \ict /update instruction count /add (40 /dac \ict jmp ufi /return to ufo /saucer torpedo movement routine etm, dap etx isp \ets /count up life jmp . 2 jmp etr 1 /expired lac \epy add \edy dac \epy swap lac \epx add \edx dac \epx dpy -i 100 etx, jmp . /return etr, dap etx /reset dzm \ets xct etd dac \etc jmp etx /player rocket routine rkt, dap rkx lac \rks /check for collision state szm / < 0 ? jmp rka /no isp \rks /manage rocket hit state jmp rke idx \rks lac \th0 dac \rth jmp rcw rke, add rsd sma jmp rkx rt1, lac \thc /animate collision, spin clockwise progressively dac \thn lio raa /angular acceleration rt2, sil 1s isp \thn /shift left \thd times jmp rt2 isp \thd jmp rt3 law i 20 /add another shift every 20 frames dac \thd lac \thc sub (1 dac \thc rt3, swap cma cli -opr /make it a clockwise spin, clear io add \rth /update rotational angle jmp rkr rka, stf 1 /start active rocket rcw, jsp . /read control word (ccw, cw, trust, fire) dio \cw /merge (or) spacewar player inputs cla rcr 4s ior \cw dac \cw lio \cw /parse and process rotation lac \rth /load angle spi /sign cleared in io? add raa /no, add angular acceleration ril 1s /next bit spi sub raa rkr, sma /normalize 0 >= angle >= 2Pi (0311040) sub (311040 spa add (311040 dac \rth /update angle ril 1s /parse thrust input spi stf 2 /set flag 2 for thrust ril 1s spi i /check next bit for fire jmp . 7 szs i 30 /sense switch 3 for single shot jmp . 4 lio \cwo ril 3s spi i /block if already set in old control word stf 3 /set flag 3 for torp jda sin /get sin, store it in \sn dac \sn lac \rth jda cos /get cos, store it in \cs dac \cs szf i 2 /flag 5 set? update dx / dy jmp rk0 lac \frc /load frame counter isp \rac /sensitivity, frame skip jmp rk1 xct ras /reset counter dac \rac lac \sn /dx cma xct rvl /apply scaling for acceleration swap /swap result into io xct rad /damping, get loop count dac \rdc rx0, swap /get intermediate value from io add \rdx /average with old dx sar 1s swap /swap into io isp \rdc /increment loop jmp rx0 dio \rdx /store updated dx lac \cs /same for dy xct rvl swap xct rad dac \rdc ry0, swap add \rdy sar 1s swap isp \rdc jmp ry0 dio \rdy jmp rk1 rk0, dzm \rac rk1, scale \sn, 4s, \sn1 /get position of rocket tip sar 2s /offset x = (sin >> 4) - (sin >> 8) cma add \sn1 dac \sn1 scale \cs, 4s, \cn1 /offset y = (cos >> 4) - (cos >> 8) sar 2s cma add \cn1 dac \cn1 lac \rpx /update pos x (add dx) add \rdx dac \rpx sub \sn1 /subtract offset for tip, store it in \px dac \px lac \rpy /same for y add \rdy dac \rpy add \cn1 dac \py scale \sn1, 1s, \rxc /store half-offset for hit detection scale \cn1, 1s, \ryc scale \sn, 6s, \sn8 /scaled sine, 8 steps sar 1s dac \sn4 /4 steps sar 1s dac \sn2 /2 steps sar 1s dac \sn1 /1 step dac \sm1 add \sn2 dac \sn3 /3 steps dac \sm3 add \sn2 dac \sn5 /5 steps add \sn1 dac \sn6 /6 steps dac \sm6 scale \cs, 6s, \cn8 /scaled cosine, 8 steps sar 1s dac \cn4 /4 steps sar 1s dac \cn2 /2 steps sar 1s dac \cn1 /1 step dac \cm1 add \cn2 dac \cn3 /3 steps dac \cm3 add \cn2 dac \cn5 /5 steps add \cn1 dac \cn6 /6 steps dac \cm6 jsp rod /display it szf i 3 /fire torpedo? jmp rkq /no szf i 1 /are we active? jmp rkq /no lac \trs sza /torpedo already active? jmp rkq /yes lac \ufs spq /saucers active? jmp rkq /no xct tlf /set up torpedo dac \trs lac \rpx /copy position dac \tpx lac \rpy dac \tpy lac \sn /dx cma xct trv /apply scaling for velocity dac \tdx lac \cs /dy xct trv dac \tdy rkq, szf i 2 /advance random on thrust jmp rki / to prevent patterned behavior of saucers random rki, lac \ict /update instruction count add (600 dac \ict rkx, jmp . /torpedo (rocket) trp, dap trx isp \trs /count up status counter (zero = inactive) jmp . 2 trx, jmp . szf i 1 jmp tr1 lac \frc /frame skip and (3 sza jmp tr1 szs 40 /sense switch 4 for high agility jmp tr2 lac \cw /check control input for rotation (ccw, cw) and (600000 sza i /steering? jmp tr1 /no tr2, lac \cs /update with steering xct trv /scale cos of rocket add \tdy /average 3 times with current dy sar 1s add \tdy sar 1s add \tdy sar 1s dac \tdy /store updated dy add \tpy /update pos y dac \tpy swap lac \sn /same for sine, dx and pos x cma xct trv add \tdx sar 1s add \tdx sar 1s add \tdx sar 1s dac \tdx add \tpx dac \tpx dpy -i /display adot and jump to return jmp trx tr1, lac \tpy /no steering, simple update add \tdy dac \tpy swap lac \tpx add \tdx dac \tpx dpy -i jmp trx /control word get routines /read control boxes rcb, dap rcx cli iot 11 rcx, jmp . /read testword rtw, dap rtx lat swap rtx, jmp . /rocket display / step rearwards - x add \sn1, y sub \cn1 / step outwards - x add \cm1, y add \sm1 / step inwards - x sub \cm1, y sub \sm1 define disp dpy-i 100 /display a dot at brightness level +1 term rod, dap rox stf 6 /set flag 6 lac \px lio \py disp rop, swap /y +4, x +6 sub \cn4 add \sm6 swap add \sn4 add \cm6 disp swap /y +5, x +3 sub \cn5 add \sm3 swap add \sn5 add \cm3 disp swap /y +6, x +1 sub \cn6 add \sm1 swap add \sn6 add \cm1 disp swap /y +6, x -1 sub \cn6 sub \sm1 swap add \sn6 sub \cm1 disp swap /y +8, x +6 sub \cn8 add \sm6 swap add \sn8 add \cm6 disp swap /y 3, x -10 sub \cn3 sub \sm3 sub \sm6 sub \sm1 swap add \sn3 sub \cm3 sub \cm6 sub \cm1 disp swap /y +7, x +6 sub \cn8 add \cn1 add \sm6 swap add \sn8 sub \sn1 add \cm6 disp szf i 6 /flag 6 set? (skip on not zero) jmp rot clf 6 /clear flag 6 lac \cm1 /invert unit vector components cma dac \cm1 lac \sm1 cma dac \sm1 lac \cm3 cma dac \cm3 lac \sm3 cma dac \sm3 lac \cm6 cma dac \cm6 lac \sm6 cma dac \sm6 lac \px /load first pos lio \py jmp rop /second pass for other side rot, szf i 2 /no, flag 2 set? rox, jmp . /no, return swap /draw exhaust flame sub \sm6 /x -11 sub \sm6 add \sm1 swap sub \cm6 sub \cm6 add \cm1 dac \px /store position dio \py lac \frc /load frame counter and (4 sza /is it zero? (state switch) jmp ro2 ro1, lac \py /state 1, display at y-1 add \cn1 swap lac \px sub \sn1 disp jmp rox ro2, lac \py /state 2, display at y+5 sub \cn5 swap lac \px add \sn5 disp jmp rox /jump to return /saucer display define sdisp B /display a dot (opt. brightness parameter) dpy -4000 B / request completion pulse term sd, dap sdx clf 5 clf 6 law i 4 dac \sdc sdl, law 5000 /y +/- 10, x +/- 4 szf 6 cma add \py swap law 2000 szf 5 cma add \px disp law 3400 /y +/- 7, x +/- 7 szf 6 cma add \py swap law 3400 szf 5 cma add \px disp law 3400 /y +/- 7, x x+/- 15 szf 6 cma add \py swap law 7400 szf 5 cma add \px disp lac \sdc /dispatch on \sdc (-4..-1) for passes (set flags) cma add (sdd dap sdd idx \sdc /increment counter and jump sdd, jmp . jmp sd1 jmp sd2 jmp sd3 stf 6 /2nd pass jmp sdl sd3, stf 5 /3rd pass jmp sdl sd2, clf 6 /4th pass jmp sdl sd1, lio \py /done, display outer dots lac (12400 /y 0, x + 21 (right side) add \px sdisp 100 lac (-12400 /y 0, x - 21 (left side) add \px ioh sdisp 100 add (1000 swap lac \udc /draw first group of dots at the left sal 1s / dispatch on \udc x 3 (udc = 0..5) for clipping add \udc add (sd4 1 dap \sd4 swap lio \py sd4, jmp . add (1000 /0 nop nop add (1000 /1 ioh sdisp add (1000 /2 ioh sdisp add (1000 /3 ioh sdisp add (1000 /4 ioh sdisp add (3000 /5, display 4 dots ioh sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (3000 /display 4 dots sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (1000 ioh sdisp add (3000 /draw group of remaining dots at the right swap lac \udc /dispatch on \udc x 3 (udc = 0..5) for clipping add (sd5 1 dap sd5 swap lio \py sd5, jmp . jmp sd0 jmp sd0 jmp sd9 jmp sd8 jmp sd7 jmp sd6 sd6, ioh /4 dots sdisp add (1000 sd7, ioh /3 dots sdisp add (1000 sd8, ioh /2 dots sdisp add (1000 sd9, ioh /last dot sdisp sd0, ioh /fetch and clear last completion pulse sdx, jmp . /return /character outline compiler /character outlines / 7 rows at 5 dots, left to right, top down, / 2 words per character, aligned left. cot, 350614 306134 /0 106041 020410 /1 350411 442076 /2 350411 406134 /3 451227 610204 /4 770207 406134 /5 350607 506134 /6 760421 041020 /7 350613 506134 /8 350613 604230 /9 105214 376142 /A 750617 506174 /B 350604 102134 /C 750614 306174 /D 770207 502076 /E 770207 502040 /F coa, . 21/ /space for addresses of character object code cgy, 10000 /unit for character grid y cgx, 6000 /unit for character grid x . 3/ /reserve space for x-unit offsets 2..4 /setup and compile digits 0-9, chars A-F ci, dap cix lac cgx /set up grid units (1..4) add cgx dac cgx 1 add cgx dac cgx 2 add cgx dac cgx 3 ci2, dzm \t1 /compile outlines law ctb dap ci4 law coa dap ci5 cla ci3, dac . 4 /fix up character index ci4, law . /start address of compiled code (fixed up) ci5, dac . /store start address (fixed up) jda cc 0 /character index (fixed up) dap ci4 idx ci5 idx \t1 sas (21 jmp ci3 cix, jmp . /character outline compiler / call law addr, jda cc, char-index (see cot) / returns next address in ac, compiled code will exit at cdx define comp A /push instr. A to object code lac A dac i cc idx cc term define cadd A /add instr. A to ac and push to object code add A dac i cc idx cc term cc, 0 dap ccx lac i ccx /get char-index sal 1s add (cot dac \cwd /addr. of first code word idx ccx /fix up return address stf 6 /flag 6 for first dot law i 22 dac \cbi /bit counter lio i \cwd /get first code word to parse comp (lio cy /- reset y dzm \cyi comp (lac cx /- reset x dzm \cxp /current x ccr, dzm \cxi /new row spi i /is there a dot? jmp ccs /no lac \cxi /get offset to last x sub \cxp sza i jmp ccd spa jmp ccm cadd (add cgx-1 jmp cca ccm, cma cadd (sub cgx-1 cca, lac \cxi dac \cxp ccd, szf 6 jmp cd1 comp (ioh /- wait for display cd1, comp (dpy -4000 100 /- display a dot clf 6 ccs, ril 1s /advance to next bit isp \cbi jmp cci law i 22 /fetch next code word dac \cbi idx \cwd lio i \cwd cci, idx \cxi sas (5 /end of a row? jmp ccr 1 /no idx \cyi /next row sad (7 /was it the last one? jmp ccz /trail and exit comp (rcr 9s /- swap comp (rcr 9s comp (sub cgy /- increment pos y comp (rcr 9s /- swap comp (rcr 9s jmp ccr ccz, szf 6 jmp cz1 comp (ioh cz1, comp (jmp cdx ccx, jmp . /return cx, 0 cy, 0 /display a character / call law char-index, jda cd (coors in cx, cy) cdp, 0 dap cdx law coa add cdp dap . 1 jmp i . cdx, jmp . constants variables ctb, /start address for code of outline compiler start 4
(~ 1.68 K 18-bit words, 03300 locations incl. variables, + JIT compiled code from 03300 – 05676, ~ 1.25 K, 02476 locations.)
That's all for this post.
▶ Next: Episode 10: AI and the Problem of Missing Accurately
◀ Previous: Episode 8: The Saucers Strike Back
▲ Back to the index.
2016-10-28, Vienna, Austria
www.masswerk.at – contact me.
— This series is part of Retrochallenge 2016/10. —