Star Lanes — Cleanroom Game Specification

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.


1. Overview

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.


2. Components

2.1 The Board (Galaxy Map)

Cell Value Symbol Meaning
1 . Empty space
2 + Unattached outpost
3 * Star (natural celestial body)
4–8 AE Company tile (company index + 3)

2.2 Companies

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.

2.3 Players


3. Game Structure

3.1 Turn Order

3.2 Phases of Each Turn

Each turn has three sequential phases:

  1. Move generation — Five candidate board positions are randomly selected and presented to the active player.
  2. Placement — The player selects one of the five candidates and places a tile, triggering one of four possible game events.
  3. Stock transaction — Dividends are paid, then the player may purchase shares in any active company.

4. Move Generation

4.1 Generating Candidates

At the start of each turn, 5 candidate positions (R(i), C(i)) for i = 1..5 are generated. Each candidate is chosen by:

  1. Pick a random row: R = INT(9 * RND + 1) (range 1–9).
  2. Pick a random column: C = INT(12 * RND + 1) (range 1–12).
  3. Reject and retry if any of the following:

4.2 Merger-Filtering Rule (Candidate Rejection)

A candidate position is rejected during generation (causing a re-roll) if all of the following hold:

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.

4.3 Displaying Candidates

Candidates are displayed to the player as row-column pairs (e.g., 7E, 3A).

The player may, at any input prompt:


5. Placement and Game Events

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:

5.1 Isolated Outpost

Condition: All four orthogonal neighbors have value ≤ 1 (empty space or out-of-bounds, treated as empty).

Effect:

5.2 Extend an Existing Company

Condition: Exactly one active company (value > 3) is adjacent, and no merger condition exists.

Effect:

5.3 Form a New Company

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:

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.

5.4 Merger

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:

  1. 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.

  2. Merge bonus paid to all players. Each player i receives a cash bonus equal to:

  3. 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:

  4. Company stats merge.

  5. Board update. All cells on the board with the loser's tile value are relabeled to the survivor's tile value.

  6. Loser is reset (becomes defunct):

  7. Stock split check on the survivor after merging.

  8. 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.

  9. The placed cell is set to the survivor company's tile value.


6. Stock Transactions (Post-Placement Phase)

6.1 Dividends

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.

6.2 Stock Purchases

For each active company i (in order 1–5, skipping inactive ones where Q(i) = 0):

During stock purchase prompts, the player may also type MAP or STOCK to view information screens.


7. Stock Splits

A stock split is triggered whenever S1(i) >= 3000 for any active company i, checked:

Split mechanics:

Note: Only one split is triggered per event, even if the resulting price is still ≥ 3000. See §11 (Bugs).


8. End of Game

The game ends after 48 total turns have been taken. At that point:


9. Display and Interface

9.1 Galaxy Map

Requested by typing MAP at any input prompt. Displays the 9×12 grid with:

9.2 Stock Portfolio Screen

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) = 100 to detect inactive companies (line 1460), but the correct active check elsewhere is Q(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.

9.3 Special Announcements

Printed with a bell character (placeholder comment at line 7900) and the header SPECIAL ANNOUNCEMENT!! for:


10. Move Validity

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.


11. Bugs and Anomalies

The following bugs and notable anomalies were identified by reading the source code:


BUG-01: Stock Split Does Not Loop — Multiple Splits Possible but Not

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.


BUG-02: Merger Size-Tie Resolution Is Ambiguous and Order-Dependent

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.


BUG-03: T1 Variable Collision — Split After Merger May Target

Wrong 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.


BUG-04: Candidate Rejection Logic Is Overly Broad (Move Generator)

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.


BUG-05: Array Index Out-of-Bounds Risk on Board Edges

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.


BUG-06: Stock Portfolio Display Uses Wrong Active-Company Test

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.


BUG-07: New Company Formation — Only First Defunct Company is Used

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.


BUG-08: Merger Bonus Divides by Zero if No Shares Outstanding

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.


BUG-09: Duplicate Merger Calls for 3-Way Adjacency

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.


12. Constants Summary

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.