Horse Race Game, Part 1
My first programming computer had a single line display and 8KB of memory. I converted an old 100-line game in BASIC to Swift to see how far we’ve come. This was a fun journey though nostalgia.
The Casio FX-790P was my first computer where I learned programming back in the 90s. It was a blend between a scientific calculator and a BASIC-programmable handheld computer. It featured a single line, 24-character LCD display, 83 keys, and 8KB of internal memory. It had scientific functions, the ability to calculate square roots, and it ran on a BASIC interpreter, allowing for the storage and execution of up to 10 programs!
This device came with pretty great owner’s manual, which had an extensive chapter on BASIC programing language. I wrote so many programs on this, mostly simple 10-50 lines long. The owner’s manual had two sample programs— Bubble Sort and a Horse Race Game. The game was by far the largest, most complex program I had ever seen. It was 102 lines long and took 1.3KB of memory.
That’s some unreadable code! I just typed this in blindly and hoped that I didn’t make any typos, without really understanding the logic. So for nostalgic reasons, I thought I would convert this program to Swift, and see how a modern programing language compares to this old BASIC code. Can it be done in less than 100 lines? I will start with a line-by-line mirroring of the original code. Then I will refactor to see if I can make it readable.
Step 1: Convert to Swift
Before I can convert this code, it has some commands that I would like to use as-is to preserve the original code. So I wrote a protocol for them. I don’t want to implement these methods, so protocol gives me a nice way to defer the implementation.
Now I convert the whole program, line by line, keeping even the variable names same.
This is 138 lines, not quite 102. However, the BASIC program had multiple statements on single line. I counted them and it too came out to be 138 lines!
Time to refactor.
Step 2: Arrays to Classes
Notice the following 2D arrays -
var A: [[Double]] = DIM(3, 4)
var X : [[Int]] = DIM(2, P)
“A” represent Horses, and “X” represent Players. The rows are the properties, and the column represent the instance.
A[1][j] = Position of the horse j
A[2][j] = Random number of the horse j
A[3][j] = Odds on the horse j
X[1][j] = Bet amount for player j
X[2][j] = Money remaining for player j
This is very interesting. This shows that even before Object Oriented languages, there was a need for grouping of data under a single variable. But since there were no classes or structs in BASIC, people used 2D arrays. Today we can convert them to proper classes. Lets start with Horse.
class Horse {
var position: Int = 0
var randomizer: Double = 0.0
var odds: Int = 0
}
When we use the Horse class, this becomes,
func horseRaceGame()...
var horses: [Horse] = Array(repeating: Horse(), count: 4)
...
for horse in horses {
horse.position = 0
horse.randomizer = Double.random(in: 0...1)
horse.odds = 1 + Int(pow(10.0, 1.2 - horse.randomizer))
}
I can create an initialize() method in Horse, and move this code in there.
class Horse...
func initialize() {
self.position = 0
self.randomizer = Double.random(in: 0...1)
self.odds = 1 + Int(pow(10.0, 1.2 - self.randomizer))
}
func horseRaceGame()...
horses.forEach { $0.initialize() }
Next I create the Player class.
class Player {
var money: Int
var bet: Int = 0
var chosenHorse: String = ""
init(startingMoney: Int = 20) {
self.money = startingMoney
}
func initialize() {
self.bet = 0
self.chosenHorse = ""
}
}
There was an array Y$[j] that was tracking the chosen horse for the player j. I don’t know why the original program didn’t make it as X[3][j], since its part of the player data. I moved it with Player. Now I can use this class in the code, replacing all X and Y variables.
func horseRaceGame()...
var players = Array(repeating: Player(), count: P)
...
// bet money
for (j, player) in players.enumerated() {
...
player.initialize()
if player.money == 0 { continue }
PRINT("PLAYER \(j) HAS $\(player.money)")
...
player.chosenHorse = S$[A$-1]
repeat {
...
player.bet = INPUT("PLAYER\(j) \(player.chosenHorse) MONEY")
} while player.money < player.bet
player.money = player.money - player.bet
}
Step 3: Horse Symbols
Next, I want to move the horse symbols within the Horse class.
let S$ = ["♠", "♥", "♦", "♣"]
This could be an enum, but for now let’s just make it a string field of the class.
class Horse {
var position: Int = 0
var randomizer: Double = 0.0
var odds: Int = 0
var symbol: String
init(with symbol: String) {
self.symbol = symbol
}
}
The initialize() method is separate from init() because it will be called again and again on replays. But the symbol of the horse will remain the same. I plan to change its name later to something like “newRace()” or “reset()”.
The code changes to:
func horseRaceGame()...
var horses: [Horse] = []
for symbol in ["♠", "♥", "♦", "♣"] {
horses.append(Horse(with: symbol))
}
And the rest of the code where we were using S$[j], we just use horses[j].symbol. But even better, I can remove all index j references.
// Before
for j in 1...4 {
if Int(horses[j-1].position) == 23 {
H = horses[j-1].odds
A$ = horses[j-1].symbol
}
}
// After
for horse in horses {
if Int(horse.position) == 23 {
H = horse.odds
A$ = horse.symbol
}
}
This, I believe, is another strength of classes over arrays. It reduces the amount of times we have to use the indices, which results in reduced bugs due to off-by-one errors.
Step 4: Horse reference in Player
There are few things I want to move to the player class. First I want to change the chosenHorse in player from String to actual Horse instance.
class Player...
var chosenHorse: Horse?
Its optional because there a state when a player hasn’t made a choice of a horse. Also a player can choose to sit out a race, in which case chosenHorse will be nil. Now, anywhere we have,
player.chosenHorse
We can replace with,
player.chosenHorse?.symbol
Step 5: Player helper methods
Next, I want to create some encapsulations around player data of money and bet, so that I can make them private to player. I want to apply the principle of Tell Don’t Ask, so that the horseRaceGame doesn’t need to read or assign the money or bet data.
class Player...
private var money: Int
private var bet: Int = 0
That gave me a total of 13 errors. So I am going to work on fixing each of them one by one, until I am left with ones that cannot be fixed.
First, is the “if player.money == 0” check that is done in two place. I created a isBroke() method.
class Player...
func isBroke() -> Bool {
self.money == 0
}
We are down to 11 errors. Next is the betting loop.
betting loop...
repeat {
player.bet = INPUT("PLAYER \(j) ... MONEY ")
} while player.money < player.bet
player.money = player.money - player.bet
We can simplify this with:
class Player...
func placeBet(_ bet: Int) -> Bool {
guard bet <= money else { return false }
self.bet = bet
self.money -= bet
return true
}
}
betting loop...
var bet: Int
repeat {
bet = INPUT("PLAYER \(j) ... MONEY ")
} while !player.placeBet(bet)
This cleaned up 6 more issues! Plus I think this better since before the bet was assigned to player and then the check for “money > bet” was done. Now we only assign the bet and money after everything checks out.
Next is the winnings calculations loop.
winning calculation...
var M = 0 // money won
if player.bet > 0 { // if player made any bets
if player.chosenHorse?.symbol == A$ {
M = player.bet * H
}
PRINT("PLAYER \(j+1) ->PRIZE $\(M)")
}
player.money += M
This is another great spot to push responsibility into the Player class. Right now the outside code is “reaching in” to check bets, horses, and money — which makes it harder to change the logic later. We can simplify this as:
winning calculation...
if player.madeAnyBet() {
var M = player.collectPrize(if: winningHorse)
PRINT("PLAYER \(j+1) ->PRIZE $\(M)")
}
This requires couple methods in Player.
class Player...
func madeAnyBet() -> Bool {
self.bet > 0
}
func collectPrize(if winningHorse: Horse) -> Int {
guard
let myhorse = self.chosenHorse,
myhorse.equals(winningHorse)
else {
return 0
}
let prize = myhorse.calculatePrize(for: self.bet)
self.money += prize
return prize
}
}
Here we are chaining the guard conditions with a comma. In Swift, commas inside a guard mean “all of these must hold”. Also notice that we created two methods on Horse class as well - equals and calculatePrize.
class Horse...
func equals(_ other: Horse) -> Bool {
self.symbol == other.symbol
}
func calculatePrize(for bet: Int) -> Int {
self.odds * bet
}
}
We no longer need H, since the odds of the winning horse is obtained by the players chosen horse, if its the winner. So we can further simplify this code:
winning calculation...
var H = 0 // odds on winning horse
var A$ = "" // winning horse
for horse in horses {
if Int(horse.position) == 23 {
H = horse.odds
A$ = horse.symbol
}
}
We can make this just one liner.
let winningHorse = horses.last { $0.position == 23 }!
Better, I can make this a computed property for horses. in the We can add an extension on Array constrained to Horse like this:
extension Array where Element == Horse {
var winningHorse: Horse? {
last { $0.position == 23 }
}
}
winning calculation...
if player.madeAnyBet() {
var M = player.collectPrize(if: horses.winningHorse!)
PRINT("PLAYER \(j+1) ->PRIZE $\(M)")
}
Notice I am using ‘last’ since that is what the above for loop would have given. Now we are down to just 2 errors. Both for the exact print statements.
PRINT("PLAYER \(j+1) HAS $\(player.money)")
I am not ready to tackle print statements. So let me just create a remainingMoney() method on Player.
class Player...
func remainingMoney() -> Int {
self.money
}
All errors gone! I can also quickly make chosenHorse as private as well.
class Player...
private var chosenHorse: Horse?
This results in 2 errors. Both when a player chooses a horse:
choose horse...
player.chosenHorse = horses[A$-1]
bet = INPUT("PLAYER\(j) \(player.chosenHorse?.symbol) MONEY")
I can create a choose() method in Player. And for the second, I can just use the local horses[A$-1].
class Player...
func choose(horse: Horse) {
self.chosenHorse = horse
}
choose horse...
player.choose(horse: horses[A$-1])
bet = INPUT("PLAYER \(j) \(horses[A$-1].symbol) MONEY ")
Step 6: Horse helper methods
Lets do the same exercise with Horse. We will try to make each of its property as private, starting with randomizer.
class Horse {
var position: Int = 0
private var randomizer: Double = 0.0
var odds: Int = 0
var symbol: String
Only 1 error.
if Double.random(in: 0...1) * (0.9 + horse.randomizer / 10.0) > 0.7 {
horse.position += 1
}
We can extract this to a move() method in Horse.
class Horse...
func move() {
if Double.random(in: 0...1) * (0.9 + randomizer / 10.0) > 0.7 {
self.position += 1
}
}
}
Next we tackle this winning condition which is used in couple of places:
horse.position == 23
Which can be made a method.
class Horse...
func reachedGoal() -> Bool {
self.position == 23
}
}
Next, there is this weird logic of moving horses -
main loop...
var G = 0
repeat {
for horse in horses {
// As soon as somebody wins we stop moving horses
if G < 1 {
PRINT(csr: Int(horse.position), " ")
horse.move()
}
if horse.reachedGoal() {
G += 1
}
PRINT(csr: horse.position, "\(horse.symbol)")
}
} while G < 2
Here, G is the winners count. This loop can be made much cleaner by getting rid of the manual G counter and instead asking the collection how many horses have finished. We don’t need to manually increment and check G, since we can calculate it each round. Secondly, in the original logic, as soon as one horse reaches the goal, the others stop moving. That means only one horse ever actually finishes, so G will never reach 2. The original BASIC trick was: Keep looping until the first horse finishes, then stop moving horses but still draw all horses once more, then exit after every horse has been drawn in that final round. However, the redraw was at the exact same position. The equivalent Swift can be written much more directly, like this:
var noWinner = true
repeat {
for horse in horses {
if noWinner {
PRINT(csr: Int(horse.position), " ")
horse.move()
}
if horse.reachedGoal() {
noWinner = false
}
PRINT(csr: horse.position, "\(horse.symbol)")
}
} while noWinner
Now it reads almost like English: while there is no winner → move horses, once a winner appears → stop moving, but still finish drawing. We can make noWinner derived from the horses array instead of mutating a flag variable. We add another property to the extension on Array for Horse like this:
extension Array where Element == Horse {
var winningHorse: Horse? {
last { $0.reachedGoal() }
}
var hasNoWinner: Bool {
winningHorse == nil
}
}
Then the loop looks even cleaner:
repeat {
for horse in horses {
if horses.hasNoWinner {
PRINT(csr: Int(horse.position), " ")
horse.move()
}
PRINT(csr: horse.position, "\(horse.symbol)")
}
} while horses.hasNoWinner
Step 7: Fixing Replay
When converting the program to Swift I had intentionally made wrong implementation of the replay logic. In the BASIC code, it had a GOTO statement, which took the program, not to the very top, but to the middle after all the horses and players were created. This preserved the remaining money of the players, while re-setting everything else (horse position, odds, player bets, etc.). But in Swift, since its a one long method, and there is no GOTO in Swift, I just recalled the horseRaceGame() from the top. That will reset the race number back to 1 and player money back to $20.
replay...
var F = 0 // Num players with 0 money
if player.isBroke() { F += 1 }
if F < P {
repeat {
var A$: String = INPUT("REPLAY (Y/N)?")
if A$ == "Y" {
R += 1
horseRaceGame() // GOTO Initialize
}
if A$ == "N" { break }
} while true
}
PRINT()
PRINT("GAME OVER")
The way to fix this is to break the single method into two methods — the top part with horses and players initialization, and the race part with odds and bets. Later we will break into even smaller methods, but for now lets start with two.
func horseRaceGame() {
var R = 1
...
race(num: 1, horses: horses, players: players)
}
private func race(num R: Int, horses: [Horse], players: [Player]) {
...
if F < players.count {
repeat {
var A$: String = INPUT("REPLAY (Y/N)?")
if A$ == "Y" {
race(num: R+1, horses: horses, players: players)
}
if A$ == "N" { break }
} while true
}
PRINT()
PRINT("GAME OVER")
}
We don’t need number of players, P, variable anymore. I also want to get rid of F, which is the number of players who have $0. That was to test the game-end scenario. I created a remaining property on Players array.
extension Array where Element == Player {
var remaining: Int {
count { !$0.isBroke() }
}
}
I can also separate the prompt and the replay and keep the loop only responsible for validating input, separating input phase from action phase.
replay...
var replay = false
if players.remaining > 0 {
repeat {
let ans: String = INPUT("REPLAY (Y/N)?")
if ans == "Y" { replay = true; break }
if ans == "N" { break }
} while true
}
if replay {
race(num: R+1, horses: horses, players: players)
} else {
PRINT()
PRINT("GAME OVER")
}
I plan to move all prompts to their own helper methods later, so this code will further clean up. Last thing I want to do in this step is rename the initialize() methods on Player and Horse to reset(), which communicates better that we are resetting for a new race.
class Horse...
func reset() {
self.position = 0
self.randomizer = Double.random(in: 0...1)
self.odds = 1 + Int(pow(10.0, 1.2 - self.randomizer))
}
class Player...
func reset() {
self.bet = 0
self.chosenHorse = nil
}
Step 8: Player and Horse IDs
One thing I noticed that in 4 loops we are still using indices.
func horseRaceGame...
for (j, horse) in horses.enumerated() {
PRINT(" \(j+1)\(horse.symbol)")
}
func race...
for (j, player) in players.enumerated() {
PRINT("PLAYER \(j+1) HAS $\(player.remainingMoney())")
...
for (k, horse) in horses.enumerated() {
PRINT(csr: (k+1)*5, " \(horse.symbol) \(horse.odds)")
}
...
var A$ = 0
repeat {
A$ = INPUT("P \(player.id)->")
} while A$ < 0 || A$ > 4
...
var bet: Int
repeat {
bet = INPUT("PLAYER \(j+1) \(horses[A$-1].symbol) MONEY ")
} while !player.placeBet(bet)
}
...
for (j, player) in players.enumerated() {
if player.madeAnyBet() {
PRINT("PLAYER \(j+1) ->PRIZE $\(M)")
}
PRINT("PLAYER \(j+1) HAS $\(player.remainingMoney())")
}
I actually had bugs where I just put j, and not j+1. So these indices really do cause off-by-one errors. Let’s add an Id property to Horse and Player.
class Horse...
private(set) var id: Int
init(id: Int, symbol: String) {
self.id = id
self.symbol = symbol
}
class Player...
private(set) var id: Int
init(id: Int, startingMoney: Int = 20) {
self.id = id
self.money = startingMoney
}
And the construction is only place we need the index.
func horseRaceGame...
var horses: [Horse] = []
for (j, symbol) in ["♠", "♥", "♦", "♣"].enumerated() {
horses.append(Horse(id: j+1, symbol: symbol))
}
var players: [Player] = []
for p in 1...P {
players.append(Player(id: p))
}
Now we don’t need indices everywhere and the rest of the code is simplified with no off-by-one bugs.
PRINT(" \(horse.id)\(horse.symbol)")
PRINT("PLAYER \(player.id) HAS $\(player.money)")
PRINT(csr: (horse.id)*5, " \(horse.symbol) \(horse.odds)")
INPUT("P \(player.id)->")
INPUT("PLAYER \(player.id) \(horses[A$-1].symbol) MONEY ")
PRINT("PLAYER \(player.id) ->PRIZE $\(M)")
PRINT("PLAYER \(player.id) HAS $\(player.remainingMoney())")
Step 9: Magic numbers
We have a lot of magic numbers through out the game that we can collect in one place under a struct GameConfig.
struct GameConfig {
static let minPlayers = 1
static let maxPlayers = 5
static let goalPosition = 23
static let startingMoney = 20
}
Then there are numbers which are used in calculating odds on a horse and how fast they move. These numbers are buried assumptions in our game logic. Lets look at them separately. First the odds —
class Horse.reset()...
self.randomizer = Double.random(in: 0...1)
self.odds = 1 + Int(pow(10.0, 1.2 - self.randomizer))
This is exponential decay formula which is often used in Sports betting models, where odds drop steeply as winning probability rises. The 10.0 is the odds base multiplier. It sets the overall curve steepness when computing odds. 1.2 is the odds exponent offset. It shifts the curve so that horses with higher/lower randomizer values don’t become too extreme.
Next we look at move —
class Horse.move()
if Double.random(in: 0...1) * (0.9 + self.randomizer / 10.0) > 0.7 {
self.position += 1
}
This is essentially threshold sampling from a uniform distribution, a very common way to implement “chance of success.” It’s a typical ad-hoc tuning pattern we see in game code: multiplying a random number, then comparing to a constant. 0.9 is the base speed factor. It sets how fast horses move on average. 10.0 is used as a randomizer divisor. It reduces the effect of the randomizer. 0.7 is the movement threshold. It determines how “hard” it is for a horse to actually move forward on a tick. The 0.7 is acting like a difficulty threshold. The (0.9 + r/10) is a scaling factor that makes the threshold easier to beat as r gets larger.
With the current values, the Odds ranges from 2:1 (best horse) to 16:1 (worst horse). And the Move probability ranges from ~22% to ~30% per turn. So horses move forward about once every 3–5 turns, and their odds can swing quite a bit depending on the randomizer.
Putting it all together.
struct GameConfig {
// Player settings
static let maxPlayers = 5
static let minPlayers = 1
static let startingMoney = 20
// Race settings
static let goalPosition = 23
// Odds calculation. Range 2-16
static let oddsBase: Double = 10.0
static let oddsWinningDampner: Double = 1.2
// Horse movement. Move probability range 22% to 30%
static let moveBaseSpeed: Double = 0.9
static let moveDampner: Double = 10.0
static let moveThreshold: Double = 0.7
}
Step 10: Prompt helpers
Next I want to move all user input prompts to their own methods. We have 4 places were we ask for user inputs.
func horseRaceGame...
var P = 0
repeat {
P = INPUT("How many players ")
} while P > 5 || P < 1
func race...
var A$ = 0
repeat {
A$ = INPUT("P \(j)->")
} while A$ < 0 || A$ > 4 // 0 means pass
if A$ == 0 { continue }
var bet: Int
repeat {
BEEP()
PRINT()
bet = INPUT("PLAYER \(j) \(horses[A$-1].symbol) MONEY ")
} while !player.placeBet(bet)
// Replay
var replay = false
if players.remaining > 0 {
repeat {
let ans: String = INPUT("REPLAY (Y/N)?")
if ans == "Y" { replay = true; break }
if ans == "N" { break }
} while true
}
Lets tackle them one at a time. First getting number of players.
extension FX790...
private func promptNumPlayers() -> Int {
while true {
let p: Int = INPUT("How many players ")
if (1...5).contains(p) {
return p
}
}
}
func horseRaceGame...
for p in 1...promptNumPlayers() { ... }
Next, choose horse.
extension FX790...
private func promptChooseHorse(for player: Player) -> Int {
while true {
let choice: Int = INPUT("P \(player.id)->")
if (0...4).contains(choice) {
return choice
}
}
}
func race...
let choice = promptChooseHorse(for: player)
if choice == 0 { continue } // 0 means pass
player.choose(horse: horses[choice - 1])
Next, placing bet.
extension FX790...
func promptBetMoney(for player: Player) -> Int {
while true {
let bet: Int = INPUT(
"PLAYER \(player.id) \(player.chosenHorse!.symbol) MONEY "
)
if bet >= 0 && bet <= player.money {
return bet
}
}
}
func race...
let bet = promptBetMoney(for: player)
player.placeBet(bet)
Lastly, replay.
extension FX790...
private func promptReplay() -> Bool {
while true {
let ans: String = INPUT("REPLAY (Y/N)?")
if ans == "Y" { return true }
if ans == "N" { return false }
}
}
func race...
if players.remaining > 0, promptReplay() {
race(num: R+1, horses: horses, players: players)
}
Step 11: Prints, Beeps and Timers
This will be the final step in this refactoring session. The code is littered with PRINT and BEEP and TIMER statement. I want to extract them out as well. In total I extracted out 12 display messages. Along with 4 prompt messages, the total comes to 16.
If we think of modern UI based applications, for instance SwiftUI, then they follow a MVVM pattern. I want to head into that direction next, where Horse and Player will become Models, The current horseRaceGame() method will become ViewModel, and these 16 display and prompt messages will turn into Views. However, this post is already too long, so we will tackle the MVVM refactoring in the next part.
End of Part 1
We have come a long way. This is how the program looks at the end of part one. I think this is, at the very least, readable.
The Horse class —
The Player class —