BUGS

BUGS IN THE ORIGINAL COLOSSAL CAVE ADVENTURE

Direct analysis of advent.for - adv350-pdp10


This page documents four bugs present in the original PDP-10 FORTRAN source of Colossal Cave Adventure - specifically the adv350-pdp10 release, the definitive 350-point version authored by Will Crowther and Don Woods in 1977. All analysis is based directly on advent.for from that release. No derivative port or reimplementation has been consulted. Line number references are to that source file.

Three of these bugs appear to be undocumented in the public record. The fourth was identified by Arthur O'Dwyer and is credited below. All four are faithfully replicated in this port: correcting them would make the game behave differently from the original, which is contrary to the project's purpose.

None of these bugs are detectable through normal gameplay. They are structural defects in the FORTRAN logic that only become visible through direct source analysis, targeted testing, or specific low-probability game sequences.


BUG 1 - THE CAVE THAT NEVER CLOSES

Under a specific but entirely reachable sequence of events, the cave closing mechanism is permanently suppressed for that game instance. The cave never closes, the lamp never runs out through the closing sequence, and the endgame never fires. The game runs indefinitely.

MECHANISM

The game tracks unseen treasures via two counters. TALLY counts how many treasures have not yet been encountered. TALLY2 counts how many can never be encountered - permanently lost treasures. The initialisation loop at label 1200 sets PROP to -1 for every treasure that has a description in the data file, then builds TALLY by subtracting those -1 values:

        DO 1200 I=50,MAXTRS
        IF(PTEXT(I).NE.0)PROP(I)=-1
1200    TALLY=TALLY-PROP(I)

RARE SPICES is object 63. It has a description ("THERE ARE RARE SPICES HERE!") and so contributes 1 to TALLY. It is placed at room 127, the Chamber of Boulders, on the far (NE) side of the troll chasm. The only route to room 127 is via the troll bridge.

The cave closing countdown, CLOCK1, only decrements when TALLY has reached zero - meaning every treasure has been seen at least once:

        IF(TALLY.EQ.0.AND.LOC.GE.15.AND.LOC.NE.33)CLOCK1=CLOCK1-1

When the player crosses the troll bridge while toting the bear, the bridge collapses and the chasm becomes permanently impassable. The code at label 30310 accounts for treasures that are now permanently unreachable by incrementing TALLY2:

30310   NEWLOC=PLAC(TROLL)+FIXD(TROLL)-LOC
        IF(PROP(TROLL).EQ.0)PROP(TROLL)=1
        IF(.NOT.TOTING(BEAR))GOTO 2
        CALL RSPEAK(162)
        PROP(CHASM)=1
        PROP(TROLL)=2
        CALL DROP(BEAR,NEWLOC)
        FIXED(BEAR)=-1
        PROP(BEAR)=3
        IF(PROP(SPICES).LT.0)TALLY2=TALLY2+1
        OLDLC2=NEWLOC
        GOTO 99

The intent is correct: if SPICES has not yet been seen (PROP still -1), it can never be seen now, so TALLY2 should be incremented. The problem is the variable SPICES itself. In the initialisation block where every object is assigned its number via a VOCAB lookup, SPICES is missing:

        KEYS=VOCAB(0+'KEYS',1)
        LAMP=VOCAB(0+'LAMP',1)
        ...
        BEAR=VOCAB(0+'BEAR',1)
        CHAIN=VOCAB(0+'CHAIN',1)
C       SPICES is never assigned

In PDP-10 FORTRAN, integer variables in COMMON blocks are zero-initialised. SPICES = 0 for the entire run. The condition at label 30310 therefore evaluates PROP(0) - one slot before the valid PLACE array - which is not -1 on the PDP-10. The condition is always false. TALLY2 is never incremented for SPICES.

The consequence: if the player collapses the troll bridge before visiting room 127, PROP(63) remains -1 permanently. TALLY never decrements to zero for SPICES. CLOCK1 never ticks. The cave never closes.

TRIGGERING CONDITIONS

Cross the troll bridge (rooms 117 to 122), proceed directly to the bear in room 130, free it with food, and return across the bridge while toting the bear - without first visiting room 127 via rooms 124 and 125. This is a natural route; rooms 127 and 130 are on separate branches from the fork at room 124 and there is no reason a player would visit the spices branch first.


BUG 2 - THE CORRUPTED CARRY COUNTER

In the final cave closing sequence, the HOLDNG counter - which tracks how many objects the player is carrying - is decremented once for the liquid in a carried bottle, despite never having been incremented for it. HOLDNG goes to -1, giving the player an effective carry limit of eight items rather than seven in the endgame repository.

This bug was identified by Arthur O'Dwyer. His original analysis is at quuxplusone.github.io .

MECHANISM

HOLDNG is managed by the CARRY and DROP subroutines. CARRY increments HOLDNG and sets PLACE to -1 (the "in hand" location). DROP decrements HOLDNG when PLACE was -1, then sets PLACE to the destination:

        SUBROUTINE CARRY(OBJECT,WHERE)
        IF(PLACE(OBJECT).EQ.-1)RETURN
        ...
        HOLDNG=HOLDNG+1
        PLACE(OBJECT)=-1
        RETURN

        SUBROUTINE DROP(OBJECT,WHERE)
        IF(PLACE(OBJECT).EQ.-1)HOLDNG=HOLDNG-1
        PLACE(OBJECT)=WHERE
        RETURN

Liquids - WATER and OIL - are never passed through CARRY or DROP directly. When the player picks up a bottle containing liquid, the bottle goes through CARRY (HOLDNG+1), but the liquid is simply assigned PLACE=-1 directly:

        CALL CARRY(OBJ,LOC)
        K=LIQ(0)
        IF(OBJ.EQ.BOTTLE.AND.K.NE.0)PLACE(K)=-1

HOLDNG is never incremented for the liquid. This is intentional and correct - the liquid is inside the bottle, not a separately carried item. The death handler accounts for this correctly by zeroing out both liquids before the drop loop runs, preventing DROP from decrementing HOLDNG for them:

        PLACE(WATER)=0
        PLACE(OIL)=0
        IF(TOTING(LAMP))PROP(LAMP)=0
        DO 98 J=1,100
        I=101-J
        IF(.NOT.TOTING(I))GOTO 98
        K=OLDLC2
        IF(I.EQ.LAMP)K=1
        CALL DROP(I,K)
98      CONTINUE

The final cave closing sequence at label 11000 does not have this guard. It calls PUT on the bottle (which calls MOVE, which calls DROP), placing the bottle in the repository at room 115 and decrementing HOLDNG for it. But PLACE(WATER) is still -1 if the bottle was carried full. When the destroy loop then reaches WATER, TOTING returns true (PLACE=-1), DSTROY calls MOVE(WATER,0), which calls DROP(WATER,0), which decrements HOLDNG again - for an increment that never happened:

        PROP(BOTTLE)=PUT(BOTTLE,115,1)
        ...
        DO 11010 I=1,100
        IDONDX=I
11010   IF(TOTING(IDONDX))CALL DSTROY(IDONDX)

HOLDNG ends at -1. The MAXCAR check (seven items) compares against -1, so the player can pick up all eight repository objects without triggering the carry limit.

TRIGGERING CONDITIONS

Enter the final closing sequence (triggered when all treasures have been deposited and CLOCK1 reaches zero) while carrying a bottle that contains water or oil. The bottle must be in the player's inventory, not placed in a room.


BUG 3 - THE HINT TIMER THAT NEVER RESETS

The bird hint (hint 5) uses the wrong branch label on a failed condition check, causing its timer to accumulate across turns where conditions are not met. The hint fires earlier than intended once conditions are met.

MECHANISM

Each hint has a threshold: if the player has been in the relevant situation for enough consecutive turns, HINTLC(hint) reaches the threshold and the hint fires. If conditions are not met on a given turn, HINTLC is reset to zero and the clock starts again. The dispatch table at label 40000 handles this with two branch targets: 40020 resets HINTLC, 40030 takes no action.

Every hint uses GOTO 40020 on a failed condition - except hint 5:

40400   IF(PROP(GRATE).EQ.0.AND..NOT.HERE(KEYS))GOTO 40010
        GOTO 40020
40500   IF(HERE(BIRD).AND.TOTING(ROD).AND.OBJ.EQ.BIRD)GOTO 40010
        GOTO 40030
40600   IF(HERE(SNAKE).AND..NOT.HERE(BIRD))GOTO 40010
        GOTO 40020

Hint 4 (label 40400) and hint 6 (label 40600) both go to 40020 on failure, resetting the counter. Hint 5 (label 40500) goes to 40030, which is a no-op fall-through. HINTLC(5) is never reset. If the player partially satisfies the bird hint conditions across non-consecutive turns, the counter accumulates, and the hint fires sooner than the threshold implies.

This is almost certainly a transcription error - 40030 instead of 40020. The comment in the source acknowledges both labels exist and serve different purposes, but no other hint uses 40030 on a failed condition.

TRIGGERING CONDITIONS

The bird hint fires when the player is carrying the rod in the presence of the bird and attempts to pick it up. The timer anomaly becomes observable if the player intermittently satisfies two of the three conditions (rod present, bird present, OBJ=BIRD) across multiple turns without fully triggering the hint.


BUG 4 - THE DRAGON'S SILENT INPUT

When the player attempts to kill the dragon barehanded, the game asks for confirmation using a raw GETIN call rather than the YES() subroutine used everywhere else. Any response that is not Y or YES causes the turn to be consumed silently with no output.

MECHANISM

YES() is the standard confirmation handler throughout the game. It prints a prompt, reads input, returns true on Y/YES, false on N/NO, and loops with "PLEASE ANSWER THE QUESTION." on anything else. The dragon confirmation bypasses it entirely:

        CALL RSPEAK(49)
        CALL GETIN(WD1,WD1X,WD2,WD2X)
        IF(WD1.NE.'Y'.AND.WD1.NE.'YES')GOTO 2608

RSPEAK(49) prints the "ARE YOU SERIOUS?" prompt. GETIN reads raw input. If the response is not Y or YES, control jumps to label 2608 (the main turn loop) with no output - the turn is silently consumed. There is no "PLEASE ANSWER THE QUESTION." fallback and no indication that anything happened.

Whether this is a bug or an intentional dramatic choice is ambiguous. The dragon confrontation is the game's most theatrical moment. It is possible the authors wanted crisp, binary behaviour here without the safety net of the YES() loop. However, the deviation from the consistent pattern used everywhere else in the source suggests an oversight.

TRIGGERING CONDITIONS

Attack the dragon in the secret canyons (rooms 119 or 121) before it has been killed. When prompted for confirmation, enter any word other than Y or YES.


NOTES ON METHODOLOGY

These bugs were identified through line-by-line reading of advent.for during the construction of this port. The approach of mapping PHP directly to FORTRAN labels - rather than reimplementing the game logic from behavioural observation - makes structural defects visible that would otherwise be undetectable. A port written from gameplay observation alone would silently correct all four bugs, producing a game that behaves differently from the original in edge cases that most players will never encounter.

The SPICES and HOLDNG bugs in particular are invisible to gameplay-based testing. The SPICES bug only manifests in a specific game instance after a specific triggering sequence, and its effect (the cave not closing) would likely be interpreted as a feature. The HOLDNG bug affects only the endgame repository and only when a bottle is carried, and its effect (an extra carry slot) would go unnoticed unless the player was specifically testing the carry limit.

The test suite for this port contains over 50,000 assertions against known game states. Several of these specifically verify that the buggy behaviour is reproduced correctly - that TALLY2 is not incremented at label 30310, that HOLDNG reaches -1 in the closing sequence with a full bottle, that hint 5's counter is not reset on a failed condition, and that the dragon confirmation consumes a turn silently on non-yes input.

Credit to Arthur O'Dwyer for the original identification of the HOLDNG desync (bug 2). The remaining three bugs are believed to be previously undocumented.