Horserace

Developer(s): Paul Stanley
Date: 198x
Type: Program
Platform(s): TS 2068
Tags: Game

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:

  1. Lines 1–10: Initialization, title screen (GO SUB 7000), and UDG setup (GO SUB 9000).
  2. Lines 20–120: Per-race setup — draw the track, then call betting (GO SUB 1999) and race start (GO SUB 1000).
  3. Lines 1000–1095: The race loop, finish detection, photo-finish logic, and winner announcement.
  4. Lines 1100–1150: Per-punter payoff/loss accounting and feedback.
  5. Lines 1999–2050: Betting input subroutine (also decrements the race counter).
  6. Lines 6000–6040: End-of-meeting summary and restart.
  7. Lines 7000–7020: Title screen display.
  8. Lines 8000–8050: Bankruptcy handler — offer elimination or $50 bailout.
  9. Lines 9000–9999: UDG definition via READ/POKE into USR "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
10.80
20.77
40.74
80.71
160.68
320.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 printing h(4) but is written with a string subscript syntax error — " "(4) indexes into a literal string rather than printing h(4). The fourth horse in a four-way tie is therefore never displayed correctly.
  • Line 1005: All six horses iterate sequentially in a single FOR c=1 TO 6 loop 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 does GO TO 20 which re-calls GO 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/2 and INK c are 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 a is reused both as the punter count (line 3) and as the loop variable limit later, while a$ is a separate string — not a naming conflict, but the reuse of a could cause confusion.

Notable BASIC Idioms

  • The PAUSE 0 followed by no INKEY$ check at lines 1081 and 1095 is used as a timed pause (130 frames ≈ 2.6 seconds) rather than an indefinite keypress wait.
  • INVERSE 1 is used inline within PRINT statements to highlight key prompts such as “C” and “X” in the bankruptcy screen.
  • The title screen at lines 70007020 uses 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.