Creating a Bowling Game using the Common Lisp Object System
The Common Lisp Object System (CLOS) was developed to create an object-oriented extension for the Common Lisp language. CLOS became the first ANSI standardised object-oriented language. CLOS enables definining objects such as classes and associated functions which can be helpful for defining behaviour in applications and enabling inheritance and data sharing between objects.1 There are many features of CLOS but this post will keep things fairly simple, by creating a class and defining some functions to keep track of the score for a bowling game.
The functions and game logic are adapted from The Bowling Game Kata by Robert Cecil Martin, which was written in Java and designed to explain test-driven development (TDD) principles. We will re-implement this bowling game here but in Common Lisp using CLOS.
Example of a CLOS Class
To define a new class, the macro defclass
can be used, which takes a list of optional super classes,
as well as a list of slot specifications and class specifications:
defclass class-name list-of-super-classes
list-of-slot-specifications class-specifications
For our example game,
we will define a class named bowling-game
.
We will leave the list of super classes empty, i.e. we will not inherit from other classes.
We will specify three slots: total
, rolls
, and current-roll
,
which will help us to track the game state.
Specifically, the slot total
will be used to store the total score of the game;
the slot rolls
will be used to store the number of pins knocked down in each roll, and
the slot current-roll
will be an index to track the current roll in the game.
In the list of slot specifications,
the :initarg
form is used to set the variable name used when initialising the class and
the :accessor
form is used to set the variable name when accessing the class.
We will use the same name for these variables
as the slot name
as it means that we will not need to keep track of different names in the code.
The :initform
variable is used to set the initial value of the slot,
provided
no :initarg
argument is passed to the function make-instance
(which will be described later)
that is used to create a new instance of the class.
For our bowling-game
class,
we will initally set the total
to 0
,
and we will do the same for the index current-roll
.
For rolls
,
we will later define a method for the generic function initialize-instance
which can be used to specify actions
to be taken when an instance is initialised.
As such, we will just provide an :accessor
argument for this slot for the time being.
Finally, there will be a class specification for the documentation string:
(defclass bowling-game ()
((total
:initarg :total ;; name for slot when initialising
:initform 0 ;; initial value of variable
:accessor total) ;; name for slot when accessing
(rolls
:accessor rolls)
(current-roll
:initarg :current-roll
:initform 0
:accessor current-roll))
(:documentation "Score a bowling game"))
We can create an instance of our class by passing the class name as an argument to the make-instance
function:
(setf new-game (make-instance 'bowling-game))
We can see various details about an instance by calling describe
on our instance:
(describe new-game)
#<BOWLING-GAME {10031C0E53}>
[standard-object]
Slots with :INSTANCE allocation:
TOTAL = 0
ROLLS = #<unbound slot>
CURRENT-ROLL = 0
We can see that the initial values for the slots total
and current-roll
are set to 0,
and the value for the slot rolls
is currently unbound because we did not provide an :initform
argument for this slot in the class definition.
If we wanted to change the initial value for one of our slots,
we can do so by passing the name used for the :initarg
of that slot to make-instance
.
Let’s say we had a beginner player and we wanted to give them a slight advantage by adding 50 to their initial total score:
(setf easy-game (make-instance 'bowling-game :total 50))
Generic Functions
Once we have created our bowling-game
class,
we can define some functions that can be used by it.
A generic function is a lisp function
with an associated set of methods
and dispatches them when it’s invoked.2
In contrast to an ordinary function defined with defun
,
a generic function may have multiple methods with the same name,
and a specific method is run
based on whether the arguments passed to the method fulfil certain criteria.
Constraining methods to be only run when the appropriate arguments are passed
means that the method is “specialized”
and when there are multiple methods with the same name,
the specialized method will take precedence over the default one.
In CLOS,
methods can be specialized for more than one argument or class (in which case, it is referred to as a multi method).
A benefit of this is that methods do not have to live inside their classes.
Methods can be created using the defmethod
macro.
Note that
the first time we define a method using defmethod
with a given function name, a generic function is automatically created with that name as well,
which will contain this method as well as any others that we define.
In our functions,
we will introduce a specializer that will
ensure that the argument g
passed to the function
is a class or subclass of the bowling-game
class:
(defmethod foo ((g bowling-game)) ;; specializer
function-body)
We mentioned that we can initialise the values for certain slots in the class definition.
However, if one needs more control over the initialisation,
we can define a method on the generic function initialize-instance
,
which is called by make-instance
.
We will create a method to initialise our rolls
slot.
Specifically,
we can define an :after
method on initialize-instance
that sets the value of the rolls
slot to be a list containing 21 zeros
as a game of bowling can contain up to 21 rolls:
each game consists of 10 frames,
with 2 rolls per frame.
If the player rolls a spare or a strike in the tenth frame,
they get an extra roll to complete the spare/strike.
(defmethod initialize-instance :after ((g bowling-game) &key)
(setf (rolls g) (make-list 21 :initial-element 0)))
In the above,
we are specialising our method to accept an argument g
that must be a class or subclass of bowling-game
.
The &key
argument is used in the initialize-instance
method to maintain consistency between the parameter list of this method and the generic function initialize-instance
.
We can access the value of the rolls
slot of the class g
(representing an instance of our bowling-game
class) using (rolls g)
.
Slot values can be changed using the setf
special form.
Now if we create a new instance of our game,
this initialize-instance
method will be called to set the value of rolls
to be 21 zeros:
(setf new-game (make-instance 'bowling-game))
(rolls new-game) => (0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0)
Now that we have initialised our class with the right values, we will introduce a number of functions that will keep track of the game state, i.e. update the game after each roll, and calculate the score of the game after it is complete. We will also introduce some functions that will be useful for the control flow in our game, and will help us to avoid cluttering the main scoring function with conditional expressions and list-indexing operations.
In the following paragraphs we will describe each of these functions (as well as some of the Common Lisp special forms they use):
The function roll will set the element of rolls
at the index current-roll
to the number of pins that were knocked down.
Again, we will include a specializer to ensure the argument g
is of type bowling-game
but we will also pass an additional argument pins
that denotes the number of pins knocked down
in the current roll
(the value for pins
should be an integer in the range of 0 - 10, but right now, our program is not ensuring that).
Given that we are passing an instance of the class bowling-game
to this function,
we can access the slot values of this class with the macro with-accessors
,
passing it a list of slot variables we would like to access in this function,
from the class g
.
Specifically, in each list, e.g. (rolls rolls)
or (current-roll current-roll)
,
the first element of each list is a local variable bound to the value of the second element,
which is the value
of the slot with that given name,
for the instance g
of bowling-game
.
Once these variables are bound inside the with-accessors
macro,
we can directly read from them or write to them.
We can use the special form elt
to access the element of a list at a specific index, e.g. (elt '(1 2 3) 0) => 1
.
We use setf
to set the appropriate element in the list
to the number of pins
knocked down.
Finally, we can increment the current-roll
slot using the incf
special form
which by default, adds one to the current value of the variable:
(defmethod roll ((g bowling-game) pins)
(with-accessors ((rolls rolls)
(current-roll current-roll))
g
(setf (elt rolls current-roll) pins)
(incf current-roll)))
We will briefly describe the helper functions
which will be used to obtain the number of pins knocked down at various frames.
The function sum-of-rolls-in-frame receives an additional argument frame-index
– the index of the current frame
and adds together the values for the first and second rolls of the frame.
We will use the special form nth
to access the nth value of a 0-indexed list, e.g. (nth 2 '(a b c d e)) => C
:
(defmethod sum-of-rolls-in-frame ((g bowling-game) frame-idx)
(with-accessors ((rolls rolls))
g
(+ (nth frame-idx rolls) (nth (+ frame-idx 1) rolls))))
The function strike-bonus also receives the frame-idx
argument and uses it
to add together the values of the next two rolls.
We will later see that if a strike is rolled,
the player gets an extra 10 points added to the number of pins knocked down in the next two rolls.
(defmethod strike-bonus ((g bowling-game) frame-idx)
(with-accessors ((rolls rolls))
g
(+ (nth (+ frame-idx 1) rolls) (nth (+ frame-idx 2) rolls))))
The function spare-bonus is similar to strike-bonus
except that it returns the number of pins knocked down in the first roll of the next frame:
(defmethod spare-bonus ((g bowling-game) frame-idx)
(with-accessors ((rolls rolls))
g
(nth (+ frame-idx 2) rolls)))
The function is-strike returns True
if the current roll is equal to 10
and Nil
otherwise.
Similarly,
the function is-spare returns True
if the current frame is equal to 10
and Nil
otherwise.
Both function use the =
special form
which can be used to compare if two integers are equal, e.g. (= 1 1) => T
and returns Nil
otherwise, e.g. (= 1 2) => NIL
(defmethod is-strike ((g bowling-game) frame-idx)
(with-accessors ((rolls rolls))
g
(= (nth frame-idx rolls) 10)))
(defmethod is-spare ((g bowling-game) frame-idx)
(with-accessors ((rolls rolls))
g
(= (+ (nth frame-idx rolls) (nth (+ frame-idx 1) rolls)) 10)))
The function score will contain the main logic for the game.
For each frame,
we can check whether the frame is a strike, a spare or a regular roll (if strike do … elif spare do … else do).
We will briefly describe how we can achieve this behaviour in Common Lisp.
In Common Lisp, there is no elif
statement.
When an if
statement is used,
the first expression below it will be run if the condition is True
and the second expression will be run if the condition is False
or Nil
:
(if (= (+ 1 1) 2)
(format t "numbers add to 2") ;; True
(format t "numbers do not add to 2")) ;; False
We can add the equivalent of an elif
statement by placing another if
statement in the expression that
is called in the case of the first if
statement returning a False
.
We can achieve the behaviour of if-elif-else
by
writing the else
expression we want to run in the False
expression of the nested if
.
One other thing to note is that we can use the progn
special form
to allow multiple expressions to be evaluated
for each True
or False
expression:
(if (= (+ 1 1) 2)
(progn ;; True
(format t "numbers add to 2~%")
(format t "writing more text..."))
(progn ;; False
(format t "numbers do not add to 2~%")
(format t "writing more text...")))
Now that we have described how to check if the current frame is a strike, a spare or a regular frame, we will describe the full score function.
We will use the loop
macro to loop over the 10 frames of the game,
also initialising a variable frame-idx
, initially set to 0,
which will point to the appropriate index of our rolls.
At each iteration of the loop,
we will check whether
the frame at frame-idx
is a strike, a spare or a regular roll.
If the frame is a strike,
we will add 10 to the next two rolls and increment frame-idx
by one position to get to the next frame.
If the frame is a spare,
we will add 10 to the first roll in the next frame and increment frame-idx
by two positions to get to the next frame.
Otherwise,
we will just add the number of pins knocked down in the current frame and increment frame-idx
by two positions to get to the next frame:
(defmethod score ((g bowling-game))
(with-accessors ((total total))
g
(loop with frame-idx = 0
for frame from 1 to 10 do
(if (is-strike g frame-idx)
(progn
(incf total (+ (strike-bonus g frame-idx) 10))
(incf frame-idx 1))
(if (is-spare g frame-idx)
(progn
(incf total (+ (spare-bonus g frame-idx) 10))
(incf frame-idx 2))
(progn
(incf total (sum-of-rolls-in-frame g frame-idx))
(incf frame-idx 2)))))))
Tying it all together
To tie everything together,
we will define two ordinary functions, create-game and test.
The function create-game
uses the make-instance
macro to make an instance of our bowling-game
class:
(defun create-game ()
(make-instance 'bowling-game))
Finally, the test
function will run through all of the necessary methods for scoring our game:
(defun test ()
(let ((x '())
(game '(3 2 3 7 10 10 10 10 4 3 3 3 3 3 3 7 6)))
(setq x (create-game))
(loop for r in game do
(roll x r))
(score x)
(describe x)))
In the test
function,
we also use the let
special form to create local variables x
and game
,
where game
is an example game consisting of a list of rolls: '(3 2 3 7 10 10 10 10 4 3 3 3 3 3 3 7 6)
.
We then set x
be an instance of bowling-game
,
and when the make-instance
function is ran within create-game
,
it will run our initialize-instance
method to initialise the slot value of rolls
.
We then loop over each roll in the sample game
,
and call the roll
function to update the state of the game with the value of that roll.
After the rolls are processed,
we can call the score
function to count the overall score of the game.
In this case, the code should correctly score the game with a value of 161
.
We can analyse the state of the game using (describe x)
:
#<BOWLING-GAME {1003B04AD3}>
[standard-object]
Slots with :INSTANCE allocation:
TOTAL = 161
ROLLS = (3 2 3 7 10 10 10 10 4 3 3 3 3 3 3 7 6 0 0 0 0)
CURRENT-ROLL = 17
We can can also change the rolls for game
in test
to be a perfect game of 12 strikes '(10 10 10 10 10 10 10 10 10 10 10 10)
, in which case we should get a maximum score of 300
. Running test
again:
#<BOWLING-GAME {1003B5F993}>
[standard-object]
Slots with :INSTANCE allocation:
TOTAL = 300
ROLLS = (10 10 10 10 10 10 10 10 10 10 10 10 0 0 0 0 0 0 0 0 0)
CURRENT-ROLL = 12
Thanks for reading!
References
1 source: https://www.dreamsongs.com/Files/clos-cacm.pdf
2 source: http://cl-cookbook.sourceforge.net/clos-tutorial/
Document History
23/09/2022 - Thanks to users on /r/lisp for their feedback, which improved the code and for pointing out that we can directly read/write to variables within the with-accessors
macro.
26/09/2022 - Now using an initialize-instance
method with an :after
qualifier to initialise the value for rolls
. Also changed wording to mention “generic functions”, and noted that they contain methods.