Source: starlanes.bas (copyright 1977 by Steven Faber, written in
Altair BASIC 12/17/76)
Document type: Cleanroom reverse-engineered specification
Purpose: Describe all game rules and mechanics as derived strictly from the source code, independent of the original implementation.
Star Lanes is a turn-based, 2–4 player, economic strategy board game set in a galactic context. Players place tiles on a grid, form and grow interstellar shipping companies, buy stock in those companies, earn dividends, and profit from mergers. The player with the greatest net worth at the end of the game wins.
| Cell Value | Symbol | Meaning |
|---|---|---|
| 1 | . |
Empty space |
| 2 | + |
Unattached outpost |
| 3 | * |
Star (natural celestial body) |
| 4–8 | A–E |
Company tile (company index + 3) |
*, value 3) with probability 1/20 (i.e., when
INT(RND*20)+1 = 10).., value 1) otherwise.There are exactly 5 shipping companies, indexed 1–5:
| Index | Name |
|---|---|
| 1 | Altair Starways |
| 2 | Betelgeuse, Ltd. |
| 3 | Capella Freight Co. |
| 4 | Denebola Shippers |
| 5 | Eridani Expediters |
Each company tracks:
A company is active if its size Q(i) > 0. A company is defunct
(available for re-use) if Q(i) = 0.
B(p)).Each turn has three sequential phases:
At the start of each turn, 5 candidate positions (R(i), C(i)) for i =
1..5 are generated. Each candidate is chosen by:
R = INT(9 * RND + 1) (range 1–9).C = INT(12 * RND + 1) (range 1–12).M(R,C) > 1).A candidate position is rejected during generation (causing a re-roll) if all of the following hold:
Q(i) = 0 for some
i).
- in any direction, with no other active-company tiles adjacent — and the only adjacent company tile has value 2 (outpost) or 3 (star) in every other direction.
More precisely (mirroring the code at lines 270–337): a candidate is rejected if any single neighbor is a company tile (value > 3) while all other neighbors are non-company tiles (value < 4). This prevents the generation of "company-extending" moves when at least one company slot is still free.
Note: This rule is complex and somewhat inconsistent with the stated instructions. See §11 (Bugs) for discussion.
Candidates are displayed to the player as row-column pairs (e.g., 7E,
3A).
The player may, at any input prompt:
MAP to view the galaxy map (then re-see the list).STOCK to view the active stock portfolio screen (then re-see the list).The player selects a valid candidate position. The four possible outcomes, determined by what is adjacent (up, down, left, right) to the chosen cell, are:
Condition: All four orthogonal neighbors have value ≤ 1 (empty space or out-of-bounds, treated as empty).
Effect:
+ (value 2 — unattached outpost).Condition: Exactly one active company (value > 3) is adjacent, and no merger condition exists.
Effect:
I is determined from the adjacent company tile
(neighbor value − 3).Q(I) += 1.I + 3).S1(I) += 500.S1(I) += 100 (outpost absorbed into lane).Q(I) += 1 (company size increases by 1 for the absorbed outpost).I + 3).S1(I) >= 3000, a
2-for-1 stock split is triggered (§7).Condition: At least one adjacent neighbor is a star (value 3) or
outpost (value 2), no adjacent neighbor is an active company tile
(value > 3), and at least one company slot is available (i.e., at least
one company has Q(i) = 0).
Effect:
I is selected (the first i where Q(i) = 0).Q(I) = 1, company is now active.S(I, P) += 5.If all 5 companies are already active and the position is adjacent
to a star or outpost but no company: the cell is simply set to +
(outpost), same as §5.1.
Condition: Two or more adjacent neighbors belong to different active companies.
Effect: One or more mergers are processed. For each pair of different adjacent companies:
Determine survivor and loser. The company with the greater
size (Q) survives. The smaller company is absorbed into the
larger. In the event of a tie in size, the company appearing first in
the adjacency check order (up, down, right, left encoded as A1, A2,
A3, A4) wins. Specifically, the survivor is the company with the
highest Q among all adjacent company tiles.
Merge bonus paid to all players. Each player i receives a cash
bonus equal to:
INT(10 * (S(loser, i) / total_loser_shares) * S1(loser))total_loser_shares is the sum of all players' holdings in
the loser company.Share conversion. Each player's shares in the loser are converted to survivor shares at a 2:1 ratio (2 loser shares → 1 survivor share), rounded to nearest integer:
S(survivor, i) += INT(0.5 * S(loser, i) + 0.5)Company stats merge.
Q(survivor) += Q(loser)S1(survivor) += S1(loser) (survivor's stock price increases by
the loser's price)Board update. All cells on the board with the loser's tile value are relabeled to the survivor's tile value.
Loser is reset (becomes defunct):
S1(loser) = 100Q(loser) = 0S(loser, i) = 0 for all players.Stock split check on the survivor after merging.
Multiple mergers. If the placed cell is adjacent to 3 or 4 different companies, each distinct pairing triggers a separate merger call. The survivor of one merger may then be absorbed by the next.
The placed cell is set to the survivor company's tile value.
Before offering stock purchases, the game pays dividends to the active
player. For each company i (1–5):
B(P) += INT(0.05 * S(i, P) * S1(i))
This is a 5% dividend on the market value of each share held, paid every turn.
For each active company i (in order 1–5, skipping inactive ones where
Q(i) = 0):
B(P)S1(i)S(i, P)R3).R3 * S1(i) <= B(P)
(player has enough cash).R3 > 0:
S(i, P) += R3B(P) -= R3 * S1(i)R3 = 0, skip (no purchase).During stock purchase prompts, the player may also type MAP or STOCK
to view information screens.
A stock split is triggered whenever S1(i) >= 3000 for any active
company i, checked:
Split mechanics:
S1(i) = INT(S1(i) / 2).S(i, p) *= 2 for all players
p.Note: Only one split is triggered per event, even if the resulting price is still ≥ 3000. See §11 (Bugs).
The game ends after 48 total turns have been taken. At that point:
S1(i) * S(i, player).B(player).Requested by typing MAP at any input prompt. Displays the 9×12 grid
with:
. (empty), + (outpost), * (star),
or A–E (company).Requested by typing STOCK at any input prompt. Displays for each
active company (stock price > 100 is used as the active check in the
display subroutine at line 1460):
Bug note: The portfolio screen uses
S1(I3) = 100to detect inactive companies (line 1460), but the correct active check elsewhere isQ(I) = 0. These are almost always equivalent (since a defunct company resets to $100), but are not guaranteed to be identical in all edge cases.
Printed with a bell character (placeholder comment at line 7900) and the
header SPECIAL ANNOUNCEMENT!! for:
A player must choose from the 5 presented candidates. Entering a position not in the list results in an error message and re-prompt. There is no mechanism for passing or skipping a turn.
The following bugs and notable anomalies were identified by reading the source code:
Handled
Location: Lines 780, 1320, 1400–1430.
Description: The split check (IF S1(I) >= 3000 THEN GOSUB 1400) is
called once. The split subroutine halves the price. However, if after
splitting the price is still ≥ 3000 (e.g., price was $7,000 → splits
to $3,500), no second split occurs. The code does not loop back to
re-check. A company's stock price can legally remain above $3,000
after a split.
Location: Lines 1060–1090.
Description: When determining the merger survivor among adjacent
companies, the code scans F1, F2, F3, F4 (up, down, right, left
neighbors) and takes the one with the largest Q. In a tie, the first
encountered in scan order wins. Additionally, the variable T1 is
reused between the outer placement loop and the merger subroutine (line
1065 vs. line 780), which could cause an incorrect split check after a
merger under certain conditions.
T1 Variable Collision — Split After Merger May TargetWrong Company
Location: Lines 780 and 1320.
Description: The variable T1 is used both as the "index of company
that just hit $3000 for a split" (set at line 780 before GOSUB 1400) and
as the "survivor company in a merger" (set at line 1065 inside the
merger GOSUB). Since a split can be triggered inside the merger
subroutine at line 1320 (IF S1(T1)>3000 THEN GOSUB 1400), this works
correctly within the merger context. However, if a split is triggered at
line 780 (the outer call), T1 at that point holds the index of the
company being extended, which is correct. There is no actual collision
in practice, but the dual use of T1 is fragile and could break under
code modification.
Location: Lines 260–337.
Description: The candidate rejection loop (which prevents offering
merger-causing positions when companies are still available) rejects
positions adjacent to even a single company tile if no other
companies are adjacent. Per the in-game instructions, extending an
existing company should always be a valid move. The filter was
apparently intended only to prevent mergers when free company slots
exist, but due to a structural error (IFQ(I1)=0 THEN 340 exits the
check loop and skips the rejection entirely only when there is a free
company), the filter also blocks valid extension moves in some
circumstances. The result: in the early game when companies are
available, moves that would extend a single existing company are
incorrectly filtered out from the candidate list.
Location: Lines 400, 1317.
Description: Adjacency is checked as M(R-1,C), M(R+1,C),
M(R,C+1), M(R,C-1). For cells on the edge of the board (row 1, row
9, column 1 = A, column 12 = L), one or more of these accesses
references M(0,*), M(10,*), M(*,0), or M(*,13). The array is
dimensioned DIM M(10,13) (line 60), so indices 0 and 10 and 13 are
technically within bounds in most BASICs (0-based or with padding).
However, M(0,*) and M(10,*) and M(*,0) and M(*,13) are never
initialized. In Altair BASIC, uninitialized numeric array elements
default to 0, which is less than 1, so they will never be interpreted as
outposts, stars, or company tiles. This means edge cells behave as if
surrounded by empty space on their out-of-bounds sides, which is the
correct and intended behavior — but relies on uninitialized memory being
zero.
Location: Line 1460.
Description: The STOCK screen skips display of company I3 when
S1(I3) = 100 (the initial stock price). This is used as a proxy for
"inactive." The correct test used everywhere else is Q(I) = 0. A
company could theoretically have Q > 0 but S1 = 100 (if it was reset
through unusual merger sequences), causing it to be incorrectly hidden.
Conversely, an active company with S1 = 100 would be hidden. These
cases are unlikely but possible.
Location: Lines 660–690.
Description: When a new company is formed, the code scans I = 1 TO 5 and selects the first i where Q(i) = 0. It does not ask the
player which company they prefer to form. The company name assigned to
the new lane is entirely determined by which index was freed first, not
by player choice or any strategic consideration. This is likely
intentional design, but worth noting as a fixed rule.
Location: Line 1260–1270.
Description: The merger bonus is computed as:
INT(10 * (S(X,I) / X1) * S1(X))
where X1 is the total outstanding shares of the loser company. If X1 = 0 (no player owns any stock in the loser), a division by zero
occurs. This would crash the game on Altair BASIC. In practice this
could happen if a company was formed and then immediately merged before
any player bought stock (though the founding player receives 5 free
shares, making X1 >= 5 after formation). It is theoretically possible
if the 5 founding shares were somehow zeroed, but that path does not
exist in normal play. Low severity, but a latent crash bug.
Location: Lines 420–470.
Description: Merger checks are performed for each distinct pair of adjacent companies: (A1,A2), (A1,A3), (A1,A4), (A2,A3), (A2,A4), (A3,A4). If three or four different companies are adjacent, multiple merger subroutine calls occur in sequence. After the first merger, the board is updated (loser tiles relabeled), but the local variables A1–A4 (captured at line 400) are not refreshed between calls. Subsequent merger checks may operate on stale adjacency data. The board re-read at line 1317 updates A1–A4 inside the merger subroutine, but by then the outer loop has already committed to which pairs to merge. The result: in a 3-way or 4-way adjacency situation, mergers may be computed incorrectly or double-counted.
| Constant | Value | Meaning |
|---|---|---|
| Board rows | 9 | |
| Board columns | 12 | |
| Number of companies | 5 | |
| Starting cash per player | $6,000 | |
| Starting stock price | $100 | |
| Starting shares (player) | 0 | |
| Founder bonus shares | 5 | New company founder receives 5 free shares |
| Star adjacency bonus | $500/share | Per adjacent star when extending or forming |
| Outpost absorption bonus | $100/share | Per absorbed outpost |
| Dividend rate | 5% | Per turn, on market value of all holdings |
| Merger conversion ratio | 2:1 | 2 loser shares → 1 survivor share (rounded) |
| Stock split threshold | $3,000 | Split triggers when price ≥ $3,000 |
| Split ratio | 2:1 | Price halved, share counts doubled |
| Total game length | 48 turns | Across all players combined |
| Star probability | 1/20 (5%) | Per cell at board initialization |
End of specification.