1989 · Origin Systems

Space Rogue

This minisite was contributed by air. It’s agent-generated and needs a human editor. Clone https://github.com/gamesexplained/gamesexplained and follow kit/START.md to curate games/c64/space-rogue with me to Gold.

Every computer pilot in the Far Arm flies by the same few rules, and the engine runs them for each ship only once in 17 passes. Put any of the nine ships in a fight below and the page runs the game's own code on them, in 3D.

01 · Ships

Filled 3D ships and space stations on a C64

The flight engine draws every ship, space station and planet as solid, filled faces, and the C64's processor, running at about one megahertz, writes every pixel of them itself. The models come from fourteen files and two built into MAIN, the Sunracer and the missile. A ship is seven to eleven flat faces, and the drawer never sorts them: each face names the plane that decides whether it is drawn before the others or after. Drag the ship to turn it.

Drawn by the page's port of draw_model ($7C61) at the engine's multicolour resolution: the points built from the model's axis vectors, the faces taken in the three passes of draw_polygons ($7ED6), and each filled with set_colour's patterns ($8F80). A test, test_ships.js, runs the engine's own draw_model on all 20 models from about 6,000 eyes and gets the same faces in the same order with the same colours, and runs set_colour on every colour code. The figures beside it are the type tables at $08D5-$0CB0; the Sunracer's are the fittings copy_ship_stats ($1737) copies in, as the listing's snapshot holds them, early in a game.

A model file opens with five instructions that put the address of its header where the drawer looks and jump to the drawer (draw_model, $7C61), so the engine draws an object by calling its model like a routine. The header holds five counts and the addresses of fifteen tables. Every point is either the model's centre or another point, plus or minus one of a short list of axis vectors, and the vectors are stored as logarithms. Scaling a model to its type's size is then one addition to each logarithm (prepare_model, $7C9B), which is how the planetoid, the planets and the station share one model file, PLA.

Instead of sorting, each face carries a plane entry that names one of the model's normals and a side of it. Before drawing, face_signs ($7E66) works out which side of each normal's plane the eye is on. The first pass draws the faces on the far side of their plane from the eye, the second the faces that name no plane, the third the faces on the eye's side. A face turned away from the eye takes its back colour, and a back colour of $FF leaves it out. Draw face by face to watch the order.

A colour is a fill pattern from $F1C8: a pattern for the even rows and one for the odd rows, making the four bit pairs solid or six checkerboards of two of them. Two of the bit pairs take their colour from the screen matrix, and the face rewrites one nybble of the matrix byte, or both, in every 4-by-8 cell it touches. A face drawn later can change the colour of an earlier face's pixels in the cells they share, and the drawer leaves those clashes on screen.

Beams and collisions never look at the model. trace_beam ($2189) tests each point along the beam against a box round the target's centre, its type's radius plus 2 units each way, and check_collisions ($6AB3) tests boxes of the two radii added. Show the hit box: the fighters fit inside theirs, but the Cruiser's nose, the Vulture's wings and most of the Titan stick out, so a beam can cross a Vulture's wing tip on the screen and do no damage.

02 · Combat

Combat AI: a space battle playground

Every computer ship flies by the same rules: find the ship its faction likes least, decide whether to fight it or run, pick one of ten tactics and steer to hold the target at that tactic's angle off the nose. Put up to five ships in a fight, the most the engine has room for, press Fight, and the page runs the engine's own code on them. Drag to look round it.

The page runs the engine's main loop ($0DED) on the engine's own bytes from the listing, in a 6502 of its own: each pass runs everything the loop calls before the drawing, with nothing pressed and the Sunracer sitting still. The ships are placed as the game places an encounter's ships when the Sunracer enters a sector (enter_sector, $15A0): each at random within eight times its radius of the Sunracer on each axis, 24 units for a Wasp or a Dart and 176 for a Titan (place_object, $167F), then made with create_object ($1949). The danger level is 0, so that no other ship arrives, and the Sunracer is neutral to the pirates and the Imperium. The random numbers, which also decide where the ships start, go on from where the listing's snapshot left them until New random numbers seeds them with random_seed ($A2DC), as a new day in the game does. A fight stops when none of the ships left is an enemy of another or of the Sunracer, when the Sunracer is destroyed, or after 6,000 passes. A test, test_combat.js, holds the page's 6502 to the kit's simulator opcode by opcode, then runs the fight this section opens with and four others in both, the page's on only the bytes this page carries, and gets the same positions, headings, hit points, tactics and targets every 8 passes, the same launches, hits, rams and kills, and the same memory at the end. The models are drawn three times their size, so that the fighters show; the grid is the Sunracer's level, 100 units a square, and the lines drop each ship to it.

Choosing a target

Once in 17 passes ship_ai ($3689) runs for every computer ship; on every third of those runs a ship without a target looks for one and a ship with a target reconsiders it. The search (ai_find_enemy, $3811) takes the ship whose faction its own faction likes least, hostile before unfriendly, and a ship that finds no enemy takes the target of a ship its faction is allied to. How the four factions stand to one another is a table of sixteen bytes (faction_table, $38B1):

So two pirate ships, or two Guild ships, never fight each other, and a fight of ships of one faction is over before it starts, unless they are Manchi ships set to attack the Sunracer. Toward the player each faction takes the player's standing with it, from $072E, which the player's kills move between −2 and 2. The Manchi's entry is the byte after the Imperium's, which is also the count of the beam weapon, $FE, so the Manchi are always hostile (Discoveries). The page sets the count to 0 for its fights, which makes the Manchi neutral to the Sunracer. Tick The Manchi attack the Sunracer to leave the count alone: in the opening fight the Wasp and the Vulture, both Manchi ships, then shoot at the Sunracer between their attacks on the Imperial ships, and a Vulture on its own destroys it.

Fight or flee

With a target chosen, ai_fight_score ($3BE0) adds up a score, and a negative score sends the ship running (tactic flee):

TermAdds
to start−4
the ship's hit points, counted in eighths of its type's full count1 for under an eighth, up to 9 at full strength
the ship's rank value less the target's, halved and rounded down−5 to 4
the factions are hostile, not only unfriendly+1
the target is busy with an attacker it remembers ($0439)+2
this ship is busy with one−2
a Guild ship−1
an Imperial ship+2
a Manchi shipthe score is 2, whatever the rest

So the Manchi never run, and an Imperial ship's 2 keeps it fighting until it is nearly destroyed; a pirate or a merchant gives up sooner, and the sooner the higher its target's rank. A ship that is fighting and more than about 333 units from its target ($0861, a logarithm) pursues it; nearer, it changes tactic one time in ten that it reconsiders (ai_tactic, $375F): from cruising or pursuing to its type's own tactic, from any other to one of seven at random.

Ten tactics

A tactic is four numbers: how to steer, the angle to keep the target off the nose, a speed and a rule for the speed. The angle makes the moves: a ship closing in keeps its target 22 degrees off the nose, a circling ship 67.5 degrees, and an eluding one about 90.

steer_toward ($3D69) turns the target's direction into two angles in the ship's own frame, one across and one up or down, takes the tactic's angle off the first and divides each by the type's turning step. The quotients are how many steps the ship will turn and pitch, one step a pass on each, and the ship steers again only when it has stopped turning (ai_steer, $3CAB). Steering never rolls a computer ship. Its roll comes only from random spins: the swerve tactic, a hit, or one time in eight that it has nothing to chase (random_rates, $3CDC).

Firing

A ship that is fighting or fleeing fires when its target is under 400 units away ($08A4), the reach of a beam: 50 steps of 8 units in trace_beam. On each pass it thinks, it fires three times in ten, and three times in ten of those it launches a missile instead of firing the beam (ai_fire, $38C1). Ships and missiles share five slots (create_object, $1949), so in a fight of five ships no missile is launched until a ship has been destroyed. The fighters, the Wasp, the Dart and the Hunter, fire the beam only with the target within about 30 degrees of the nose; the larger ships, whose beam entries have the top bit set, fire it in any direction. The TAC prints ranges at four times these units, in metres (tac_range, $33F8), so a beam reaches 1,600 m.

Beam powerDamage a hitShips
110Dart and Scow
215Wasp, Corsair and Tanker
320Cruiser and Vulture
420, which the shields do not reduceTitan and Hunter

The shields halve a hit once for each step of strength, the fore shield for a hit from in front and the aft shield for one from behind (shield_reduce, $6F88). A beam, and a plasma torpedo, gets through one step of them (beam_hit, $2222), so a fore shield of 3 halves a power-3 beam twice, to 5, and an SM-1 missile's 100 three times, to 12. A hit of 10 or more, after the shields, knocks out one of the ship's systems one time in five, and that hit takes nothing off the hit points (damage, $6EA0). Ships that touch ram each other: each takes eight times their relative speed, of which only the low byte counts, and they bounce apart as in an elastic collision (ram, $6BC3). Ships on the same side ram each other too, since check_collisions ($6AB3) tests every pair and never their factions: in one of the fights test_combat.js runs, a Titan and four fighters, the Titan and the Hunter, both Imperial, collide four times.

03 · Encounters

What the ship meets depends on the system and the player's rank

Each system's map carries a danger level and five weights, one for each class of ship. When an encounter comes, the weights choose the class and the player's rank chooses the ship within it.

The chance of each ship once one is drawn, worked out by the page's port of pick_ship ($2C8C) over every roll it can make, from ship_class_types ($2D24). A test, test_odds.js, runs the engine's own routine on every roll for every system and rank and counts the same.

A roll from 0 to 99 is spent across the weights from the last class to the first. A second roll from 0 to 15 picks one of the class's four entries directly if it comes out under 4; otherwise a roll from 0 to the Sunracer's rank value, halved and kept to 3 at most, picks it. The rank value is the player's rank plus 2 ($175B), and the rank rises from 0 to 5 with kills and surrenders ($1B2B), so a Dart becomes a Corsair, a Cruiser a Titan and a Wasp a Vulture more often as the player's repute grows.

Whether an encounter happens at all is decided before this, on each step of HELM travel, from the hours the step takes and the danger level (encounter_roll, $2C1E), and in flight every 256th pass with an eighth of the danger level (frame_update, $1239). Ja-Karn, the Manchi system, has danger 85 and sends nothing but Wasps and Vultures.

04 · Flight

From the keyboard the Sunracer turns over eleven times faster than a fighter

Each press of a turn key adds a 1024th of a turn to the Sunracer's turn rate, up to 70, and the ship turns by its rates on every pass until S stops it. A computer ship turns by its type's fixed step, 2.11 degrees a pass for the nimblest.

ShipTurn a passSet by
Sunracer, keys0.35° for each step, up to 24.6°the turn, pitch and roll rates ($0516, $0508, $050F), each held between −70 and 70
Sunracer, joystickup to 3.5°the stick's eased position, through joy_rates ($24A3): rates from −10 to 10
missiles2.81°the type's step, 8/1024 of a turn
Wasp, Dart and Hunter2.11°5.99/1024
Cruiser, Vulture, Corsair and Scow1.05°2.99/1024
Titan and Tanker0.70°2/1024

test_combat.js runs turn_objects ($7634) on the engine with a rate of 1 and measures the turn: a 1024th of a turn for the Sunracer, and each type's step for the computer ships. A computer ship turns one step a pass on each of its axes while it has steps left (turn_ship, $76DD); the Sunracer turns its rates' worth on every pass (turn_by_rates, $9037).

Speed works two ways. The computer ships always fly in cruise, and so does the Sunracer until the player switches. In cruise a ship has a set speed, and cruise_thrust ($7540) moves its velocity toward the nose's direction at that speed by at most the type's thrust each pass: a Dart from a standstill reaches speed 70 in 224 passes, a Titan in 374. Speed 70 is 4.4 units a pass, and the readout shows 16 times the velocity's length (speed_of, $7C21). Newtonian flight, CTRL-E or F7, is the player's alone: the velocity changes only while + or − is held, by the thrust along the nose, so the ship keeps drifting the way it was going whichever way it turns, up to a speed of 70 (apply_thrust, $74D3). The Sunracer's thrust is its engine's, times 8 over 8 plus the cargo slots in use.

Space is a cube. Each object's position is three 24-bit numbers, a whole part of 16 bits and a fraction of 8, and its heading is three rows of three logarithms, the ship's axes as seen from outside (slot_orient, $044B). Steering uses all three axes: a ship pitches toward a target above it as readily as it turns toward one beside it. The planets, the stars and the black hole pull every ship toward them, the pull's strength from the type's scale, and the planetoids do not pull (gravity, $787B). A ship more than about 2,500 units from the Sunracer is removed on the next 256th pass (frame_update, $1239), which is how a fight that drifts away ends. In the fight on this page, the ships spread over a few hundred units on every axis.