Horserace is a multi-player betting simulation in which one to several “punters” wager money on one of six horses across a configurable number of races. Each horse’s odds are randomly assigned as powers of two (1:1 through 32:1) using the expression `2↑(INT(RND*6))`, and the race itself is animated on screen with a UDG-defined horse character moving across the display. Advancement probability decreases as the odds improve, so higher-odds horses move more slowly on average. A photo-finish routine handles ties by randomly selecting a winner from up to four finishing horses, and players who run out of money can either be eliminated or receive a $50 bailout.
Program Structure
The program is organized into a main flow section and several subroutines accessed via GO SUB or direct GO TO. The high-level flow is:
- Lines 1–10: Initialization, title screen (
GO SUB 7000), and UDG setup (GO SUB 9000). - Lines 20–120: Per-race setup — draw the track, then call betting (
GO SUB 1999) and race start (GO SUB 1000). - Lines 1000–1095: The race loop, finish detection, photo-finish logic, and winner announcement.
- Lines 1100–1150: Per-punter payoff/loss accounting and feedback.
- Lines 1999–2050: Betting input subroutine (also decrements the race counter).
- Lines 6000–6040: End-of-meeting summary and restart.
- Lines 7000–7020: Title screen display.
- Lines 8000–8050: Bankruptcy handler — offer elimination or $50 bailout.
- Lines 9000–9999: UDG definition via
READ/POKEintoUSR "a".
UDG Horse Character
Line 9000 defines UDG “A” by poking eight bytes read from a DATA statement into the address returned by USR "a". The data includes both decimal and BIN literals, creating a small horse silhouette that is then printed throughout the race using INK c to color each horse distinctly. The UDG is referenced in the source as [UDG-A].
Odds and Movement Probability
Each horse’s odds f(c) are assigned as 2↑(INT(RND*6)), giving values from 1 to 32 in powers of two. The race movement logic on line 1005 uses a chain of Boolean expressions to advance a horse by one position per iteration based on its odds:
| Odds value f(c) | Advance probability |
|---|---|
| 1 | 0.80 |
| 2 | 0.77 |
| 4 | 0.74 |
| 8 | 0.71 |
| 16 | 0.68 |
| 32 | 0.65 |
In Spectrum BASIC, boolean expressions evaluate to 1 (true) or 0 (false), so the sum of six boolean terms on line 1005 neatly selects exactly one probability branch based on f(c) while adding 0 for all non-matching terms.
Skipping Bankrupt Punters
Lines 1101, 2031, and 6015 use a compact but unusual idiom to skip over punters whose balance is zero. The pattern is:
IF p(r)=0 THEN LET r=r+1: GO TO (1101 AND r<a+1)+(1130 AND r=a+1)
This exploits the fact that boolean values are 0 or 1 numerically, so the GO TO target evaluates to the correct line number depending on whether the loop variable has exceeded its limit. It is an elegant but somewhat fragile technique that replaces a nested IF structure.
Photo-Finish Logic
When ge=1 (a horse has reached column 31), the program collects all horses at that position into array h(). If many=1 there is a single winner; otherwise the photo-finish block (lines 1055–1082) picks a winner randomly with INT(RND*4)+1 and retries if h(er)=0. However, this retry at line 1071 can loop indefinitely if fewer than four horses tied and the random index lands beyond the valid entries, since h() is dimensioned to 6 but only partially filled — the uninitialized entries will be 0, making the guard condition necessary but the loop potentially slow.
Bugs and Anomalies
- Line
1058:PRINT ;" "(4)appears to attempt printingh(4)but is written with a string subscript syntax error —" "(4)indexes into a literal string rather than printingh(4). The fourth horse in a four-way tie is therefore never displayed correctly. - Line
1005: All six horses iterate sequentially in a singleFOR c=1 TO 6loop rather than advancing simultaneously, meaning horse 6 always moves after horse 1 in each frame — a minor simulation asymmetry. - Line
1150: After paying out all punters, the program doesGO TO 20which re-callsGO SUB 1999; the race counter is decremented there rather than at the top of the race loop, which is slightly non-obvious. - Lines
105/1006:INK c/2andINK care used in different contexts for the same horse, so the colors used in setup and during the race do not match for horses 1–3. - The variable
ais reused both as the punter count (line3) and as the loop variable limit later, whilea$is a separate string — not a naming conflict, but the reuse ofacould cause confusion.
Notable BASIC Idioms
- The
PAUSE 0followed by noINKEY$check at lines1081and1095is used as a timed pause (130 frames ≈ 2.6 seconds) rather than an indefinite keypress wait. INVERSE 1is used inline withinPRINTstatements to highlight key prompts such as “C” and “X” in the bankruptcy screen.- The title screen at lines
7000–7020uses block graphic characters to render large text spelling “HORSERACE” and “MEETING” without machine code.
Source Code
1 RANDOMIZE :BORDER 7:PAPER 7:INK 0:CLS :GO SUB 7000:CLS
2 INPUT "How many races would you like there to be? ";races
3 INPUT "How many punters are there?",a
4 DIM p(a):FOR c=1 TO a
5 LET p(c)=200
6 NEXT c
10 GO SUB 9000
20 CLS :GO SUB 1999
100 FOR c=2 TO 12 STEP 2
105 PRINT INK c/2; AT c,0;c/2;" [UDG-A]"
106 PRINT AT c-1,2;"█"
107 NEXT c
108 PLOT 251,170:DRAW 0,-102
109 PRINT AT 13,2;"█":PRINT AT 0,31; OVER 1;"o"
110 LET a$="FINISH":FOR c=1 TO 6:PRINT AT 13+c,31;a$(c):NEXT c
115 FOR c=163 TO 67 STEP -16:PLOT 24,c:DRAW 227,0:NEXT c
120 GO SUB 1000
1000 BEEP .5,0:PRINT AT 14,0;"READY":BEEP .5,6:PRINT AT 14,0;"STEADY":BEEP .5,12:PRINT AT 14,0;"GO ":BEEP .5,18:PRINT AT 14,0;" "
1001 LET ge=0:DIM x(6):FOR c=1 TO 6:LET x(c)=2:NEXT c
1002 FOR c=1 TO 6
1004 BEEP .03,-20:BEEP .03,-30
1005 LET x(c)=x(c)+(f(c)=1 AND RND<.8)+(f(c)=2 AND RND<.77)+(f(c)=4 AND RND<.74)+(f(c)=8 AND RND<.71)+(f(c)=16 AND RND<.68)+(f(c)=32 AND RND<.65)
1006 PRINT INK c; AT c*2,x(c)-1;" [UDG-A]"
1008 IF x(c)=31 THEN LET ge=1
1009 BEEP .03,-20:BEEP .03,-30
1015 NEXT c
1020 IF ge=1 THEN GO TO 1050
1040 GO TO 1002
1050 DIM h(6):LET many=0:FOR c=1 TO 6
1052 IF x(c)=31 THEN LET many=many+1:LET h(many)=c
1053 NEXT c
1054 IF many=1 THEN LET winner=h(1):GO TO 1090
1055 PRINT AT 20,0;"There is photo-finish between these horses: "
1056 PRINT AT 21,15;h(1);" ";h(2);" ";
1057 IF many=3 THEN PRINT ;h(3);
1058 IF many=4 THEN PRINT ;" "(4)
1060 FOR c=1 TO 300
1065 NEXT c
1067 LET er= INT (RND*4)+1
1070 LET winner=h(er)
1071 IF winner=0 THEN GO TO 1067
1075 IF RND<.33 THEN LET g$="a short head":GO TO 1079
1076 IF RND<.4 THEN LET g$="a neck":GO TO 1079
1077 LET g$="1 length"
1079 PRINT AT 20,0;" "
1080 PRINT AT 20,1;"The winner is ";winner;"who won by":PRINT AT 21,1;g$
1081 PAUSE 130
1082 CLS
1085 GO TO 1100
1090 PRINT AT 20,1;"The winner is ";winner;"who won by ":PRINT " "; INT (RND*3)+2;" lengths."
1095 PAUSE 130:CLS
1100 FOR r=1 TO a
1101 IF p(r)=0 THEN LET r=r+1:GO TO (1101 AND r<a+1)+(1130 AND r=a+1)
1105 IF e(r)=winner THEN LET p(r)=p(r)+b(r)*f(winner):GO TO 1115
1110 LET p(r)=p(r)-b(r)
1115 IF p(r) <=0 THEN GO TO 8000
1117 IF e(r) <>winner THEN GO TO 1125
1120 CLS :PRINT AT 10,4;"Well done punter ";r;" !"'"Your horse won and you now have $";p(r):PAUSE 0:GO TO 1130
1125 CLS :PRINT AT 10,4;"Bad luck punter ";r;" !"'"Your horse did not win,and so you now have $";p(r):PAUSE 0
1130 NEXT r
1150 GO TO 20
1999 LET races=races-1:IF races=-1 THEN GO TO 6000
2000 DIM f(6):FOR c=1 TO 6:LET f(c)=2↑(INT (RND*6)):NEXT c
2020 PRINT "The tipsters reckon the betting:"
2025 FOR c=1 TO 6:PRINT INK c; PAPER (7 AND c<4)+(0 AND c>3); AT 2*c,2;"HORSE ";c;" ";f(c);":1"
2029 NEXT c
2030 DIM e(a):DIM b(a):FOR f=1 TO a
2031 IF p(f)=0 THEN LET f=f+1:GO TO (2031 AND f<a+1)+(2050 AND f=a+1)
2032 PRINT AT 14,1;"Punter ";f;":":PRINT '"You have $";p(f);". ":PRINT "How much would you like to bet?"
2033 INPUT b(f):IF b(f)>p(f) THEN GO TO 2033
2035 PRINT '"On which horse is this to placed?"
2036 INPUT e(f)
2037 IF e(f)<1 OR e(f)>6 THEN GO TO 2036
2038 PRINT AT 17,0;" "
2039 PRINT AT 19,0;" "
2040 NEXT f
2050 CLS :RETURN
6000 PRINT "THE MEETING IS NOW FINISHED"
6010 FOR f=1 TO a
6015 IF p(f)=0 THEN LET f=f+1:GO TO (6015 AND f<a+1)+(6040 AND f=a+1)
6020 PRINT '"Punter ";f;" finishes with $";p(f)
6030 NEXT f
6040 PRINT INVERSE 1;"Press any key to play again.":PAUSE 0:RUN
7000 PRINT ''" █ █ ███ ███ ███ ███ ███ ██ █ █ █ █ ███ █ ███ ███"
7010 PRINT ''" ███ █ ███ ███ ██ █ ██ ██ █ █ █ █ █ █ █ █ ███ ███"
7020 PRINT ''"A gambling game for the whole family"''" © Paul Stanley":PAUSE 300:RETURN
8000 CLS :PRINT AT 5,0;"Tut Tut! Punter ";r;", you have":PRINT "run out of money."
8005 PRINT :PRINT "I must therefore not let you betany more."
8010 PRINT '"However, if your rules are to allow cheating, if you press "; INVERSE 1;"C";:PRINT ;" Iwill give you $50.00 more."
8020 PRINT "Otherwise press "; INVERSE 1;"X"; INVERSE 0;" and I will cancel you from the game"
8030 IF INKEY$="x" OR INKEY$="X" THEN LET p(r)=0:GO TO 8050
8040 IF INKEY$="c" OR INKEY$="C" THEN LET p(r)=50:GO TO 8050
8045 GO TO 8030
8050 CLS :GO TO 1130
9000 FOR c= USR "a" TO USR "a"+7:READ v:POKE c,v:NEXT c
9001 DATA 16, BIN 00011011,18,254,62, BIN 01100110, BIN 10101010,34
9999 RETURN Note: Type-in program listings on this website use ZMAKEBAS notation for graphics characters.