A common pickup should appear more often than a rare one. Listing the common pickup several times can work, but weights make the intended balance clearer. This helper maps a draw to a weighted outcome without taking ownership of your random generator.

Include and initialise
Reserve W(8), scalar C/A/R/O/E/T and private I/U/V. Reserve lines 3000–3230, combine numbered sources using build.py and prevent fall-through with END, STOP or a jump. Use GOSUB on C64/BBC or GO SUB on Spectrum. Avoid private scratch in enclosing caller FOR loops.
C is the active outcome count, from zero to eight. W(1)..W(C) are integral weights from zero to 255; their total must not exceed 255. A is the supplied draw. Call 3000 to clear T/O/E and set R=255, retaining C/A/W. Set the weights and draw before selection.
Supply a draw in the right range
Call 3100. For a positive total T, A must be an integral number from zero to T-1. R returns a zero-based outcome index, O=1 and E=0. A zero-weight entry has no interval and cannot be selected. The helper retains every weight and the supplied draw.
For weights 1,3,0,4, the intervals are: outcome zero gets draw 0; outcome one gets 1–3; outcome two gets none; outcome three gets 4–7. There are eight possible draws. If those draws are equally likely, the outcomes occur in the stated weight ratio. The routine itself does not establish that assumption.
C=0 or an all-zero active table returns T=0,O=0,R=255,E=0 and ignores A. E=1 rejects an invalid count, weight or total, returning T=0 and no selection. E=2 rejects an invalid draw after a valid positive total has been computed; T remains available. Invalid calls never alter C/A/W. Check O before indexing R+1.
The reusable routine
Reusable numbered routines
3000 LET R=255: LET O=0: LET E=0: LET T=0: RETURN
3100 LET R=255: LET O=0: LET E=1: LET T=0
3110 IF C<0 OR C>8 OR C<>INT (C) THEN RETURN
3120 IF C=0 THEN LET E=0: RETURN
3130 LET U=0: FOR I=1 TO C
3140 IF W(I)<0 OR W(I)>255 OR W(I)<>INT (W(I)) THEN RETURN
3150 LET U=U+W(I): NEXT I
3160 IF U>255 THEN RETURN
3170 LET T=U: LET E=0
3180 IF T=0 THEN RETURN
3190 LET E=2
3200 IF A<0 OR A>=T OR A<>INT (A) THEN RETURN
3210 LET E=0: LET V=0: FOR I=1 TO C: LET V=V+W(I)
3220 IF A<V THEN LET R=I-1: LET O=1: RETURN
3230 NEXT I: RETURNThe cumulative total divides the draw range into adjacent intervals. Selection returns the first upper bound greater than A. That strict comparison makes exact boundaries belong to the next interval and naturally skips empty intervals.
Try more than one interval
The maintained caller makes two high-range selections, then selects from draw zero and tries an all-zero table. Its earlier diagnostic table tests exact boundaries, zero-weight entries, empty tables, maximum weight, invalid draws and a total above 255.
Maintained retained-state caller
90 LET C=4: GOSUB 3000
100 LET W(1)=1: LET W(2)=3: LET W(3)=0: LET W(4)=4: LET W(5)=0: LET W(6)=0: LET W(7)=0: LET W(8)=0
110 LET A=6
120 GOSUB 3100: GOSUB 2000: LET AA=0: LET BB=0
130 IF R=3 THEN LET AA=1
140 LET A=5
150 GOSUB 3100: GOSUB 2000
160 IF R=3 THEN LET BB=1
170 LET A=0: GOSUB 3100: GOSUB 2000
180 FOR KK=1 TO 8: LET W(KK)=0: NEXT KK: LET A=255: GOSUB 3100: GOSUB 2000
190 PRINT "PICK RESULTS";AA;BB
200 POKE MB+995,AA: POKE MB+996,BB: POKE MB+997,GD: POKE MB+998,WC-256*INT (WC/256): POKE MB+999,INT (WC/256): POKE MB+1000,239: STOPThe line 2000 writer, mailbox, AA/BB and JJ/KK (Z/Q on Spectrum) belong to the diagnostic. Your game provides one draw, calls 3100 and uses R when O=1.
Customise the distribution
The custom caller replaces weights 1,3,0,4 with 2,0,5,1. Both tables total eight, but several draws now choose different outcomes. Increase a weight to make its interval wider, or set it to zero to disable that outcome without changing the table’s indices. Revalidate the total after changes.
The helper calls no RND function. Use a bounded draw that matches your generator’s actual domain and your desired distribution. Reducing an arbitrary byte modulo T is biased when the source domain does not divide evenly into T buckets; a small-state generator may not even produce all 256 byte values. Rejection sampling can correct a suitable source at the cost of retries, but it must account for that source’s real domain. A range check alone does not prove randomness or fairness.
Weights are balance data, not percentages unless the chosen total is 100. Larger totals need a wider documented interface and revised diagnostics. This routine owns no loot inventory, enemy behaviour or repeated-draw history. Drawing a rare outcome does not remove it from future draws; a shuffle bag has a different purpose.
Mistake and repair
The wrong caller reduces valid high draws modulo four even though the table totals eight. Those draws move into lower intervals. Both saved high-range choices become wrong and print zero. The selector cannot recover the original draw after a caller has discarded part of it.
The repair passes the intended bounded draw unchanged. Repaired source and loaded tokenised program match the working caller exactly.

Cost and Maths for Games
Validation scans up to C weights; selection scans up to C cumulative intervals. The array uses interpreter numeric storage, not eight packed bytes. Recomputing totals makes changing weights straightforward but costs time. A precomputed cumulative table can help when weights stay fixed, with a clear rebuild rule when they change. Larger tables may justify binary search or an alias method, which spend storage/code on faster selection.
Maths for Games connects intervals, ratios, discrete probability and sampling bias. The Craft helps choose the distribution players should experience; a technically correct weighted table can still create frustrating streaks.
Machine and measured size
C64 BASIC V2 recognises only the first two significant characters of numeric variable names. Short names make the ownership visible; check for collisions before including another component. Array element zero exists but is unused here. The diagnostic uses a mailbox at $C000, outside the normal BASIC heap.
The numbered helper occupies 377 tokenised program bytes on this interpreter, including line headers. This excludes the caller, test tables, numeric scalar/array storage, interpreter stacks and ROM. Tokenised size is not machine-code size or a hardware-cycle count. No real-time or whole-game performance budget is inferred from the diagnostic. The caller’s AA/BB, JJ/KK and RAM mailbox are observation machinery, not required game state.
Execution evidence
Working, custom, deliberately wrong and exact repaired callers executed through C64’s stock BASIC interpreter under Emu198x 0.44.2. Build198x 0.2.9 tokenises C64 PRG and Spectrum TAP programs. C64 enters stock RUN, Spectrum uses ROM tape loading, and BBC enters every numbered source line through MOS/BASIC. Actual loaded program bytes, complete state packets, positive completion/count/guards and visible result pixels are checked. After the positive completion marker, the observer advances two complete display frames before capturing, so a partially scanned final character cannot masquerade as a finished picture. Firmware is not distributed.
An independent mathematical/order model checks the trace and rejects altered source, configuration, loaded program, state, completion and image evidence. Host exhaustive checks complement the native boundary/sequential examples; those host ranges are not claimed as natively exhaustive. The supplied diagnostic callers deliberately end with STOP; the interpreter report is expected. The wrong caller prints 0,0 and the normal/custom/repaired callers print 1,1.
Public CI checks retained evidence; it does not execute restricted firmware. Refresh with python3 scripts/verify-basic-selection.py weighted-choice commodore-64 --tools /path/to/extracted/releases and inspect with python3 scripts/check-basic-selection.py weighted-choice.
Explore further
Compare the assembly technique. The BASIC game-flow routine makes session boundaries explicit. Complete source and native evidence.