This video is a Redo for the MOOC.
Redo means do it yourself,
watch the video and do what is shown step by step.
It is also an invitation to look at how we program.
I won't follow a precise script,
I will try to do it, but if I encounter bugs or
things like that, I'm gonna handle them as I go along.
In this video, the idea is to program a little
language that you can find in role-playing games like
"Dungeons and Dragons". For example,
for the ones who have this expression, what
does this expression mean?
It means: you must throw 2 20-sided dice and 1 4-sided.
You'll see it can be a Pharo expression.
So in this video we're going
to implement a class representing a die and a class
representing a handful of dice.
So let's begin.
We begin with defining a package.
We call this package "Dice".
I don't really want to see
other things. So in this
video I won't code in the
debugger, you've seen it in another video, I will
do it on a case-by-case basis, in an opportunistic way.
Here I define the "Die" class which
has a certain number of "faces".
I compile. I add a class comment.
For the moment, not much thing because it is very very simple.
We are starting to enable our
object to be initialized.
I will do it like this.
I call for an initialization of the super-class,
by default I assign 6 faces to my die because it is
the most common die.
Now I'm starting to develop a
tests class, to be sure that what we do
doesn't break what has already been done.
Tests classes are sub-classes of the TestCase class.
We call it "DieTest".
I have my tests class.
One of the first tests to do, you
don't have always to do it like this, but
in any case I want to begin with a test that works well.
As for the moment we don't have many things, I say that initialization is ok.
This is also a way to show you
how you can test that you can catch exceptions
or that exceptions mustn't occur.
Here I am saying:
"Die new should not raise error".
What does it mean?
It means that when I execute this bit of code "Die
new", no error must occur.
I'm gonna classify my test and execute it.
It's green. All right.
So now, I'd like to define the
method that makes a die roll.
I know that in Pharo there must be a method
called "at Random".
At Random, what does it do?
It enables to have... Ok...
So now I look at the implementation to be sure
it's ok. AtRandom, what does it do?
It returns an integer at random from 1 to self, so it's perfect.
So I'm gonna define a new
method in Operations.
What will it do?
Roll. I say: "you return faces
atRandom".
So I write a test for this.
TestRolling. What do we do now?
We create a die. d:= die
new
And now I write "1000 timesRepeat".
What? "d roll".
And I want this to be between 1 and 6.
"Between: and:", it's
ok. "Between 1 and 6".
It is not very good because here we created
a test only for 6-sided dice, we could have said
it works depending on the number of sides of the die.
We will do it later.
So I compile.
I get an error. Here it is...
It is ok, I have my test.
Now it's time to save. Here I have my "Dice"
package, I save it locally, "Save".
I had created others before to train a little so I
create a new one "New version with
rolling and test". All right.
Ok, it is saved.
Now I'd like to change
the creation interface. First we rearrange
categories. If we want to change a little the creation
interface. We say: "to create a die
use die faces".
On this expression you must see
that faces is a message sent to the die class and not
to an instance of die class, as it is the case
in the roll method or in others methods coded until now.
I will do this for you to understand when you
have to use and go to the class level or not.
Let's begin by writing a test.
"betterInterface".
If I go on with the same logic,
"TestbetterCreationInterface",
Here I'd like to do something like this for instance,
and this to be faces.
I will do it slowly.
I go there and I type "instance creation", faces: , anInteger.
I could write it in a short way but here
I do it in a calm way.
I create a die.
I write "self new", as self here is the die class itself.
I tell: "create an instance".
And now with this instance I use
an accessor to assign it the value passed as an argument.
Obviously, I return the die that has just been created.
When the code will be executed, it won't work because
faces doesn't exist, so don't worry.
You see that the test isn't ok, but
it's normal, if I execute this
for example, if I do debug to see...
And I click on Over, here it says: "I don't know
the faces message."
Here we will do it calmly, I won't do it in the debugger.
I say: "that's true, I have to add an accessor
here. So I write faces: anInteger.
And there I write : faces := anInteger.
And while I'm at it, I create the read accessor.
I return this one.
And here my test is green.
So we save, "save"
"better die creation method with tests".
All right.
Now we can start to
define what we want for
diceHandle. Basically if we look, diceHandle,
how would we like to write it?
We would like to write diceHandle new addDie.
So now we are going to create a new die, "die faces 6, addDie".
"Die faces 10".
We start to write a
test class, this time.
So a new class which inherits from TestCase.
All right. I have my new tests class.
And I define a test.
The idea is to create a handful and to
check there are the right dice in it.
I write "testAdding", I want to reuse my
code, there is no reason otherwise.
So I have my
handle; yourself , because I want to
get the message receiver, it is to say the handle
and not the argument that is here.
Now what should I do?
I write "self assert h diceNumber
equals 2".
I compile. Obviously the system says: "I don't
know the DiceHandle variable. Do you want it
to be a class?"
Yes. It must be a class.
Here it will define it.
As I know that I have to stop the dice anyway
I take this opportunity to
put an instance variable.
I compile all this.
Now it's red because "Add die" hasn't been defined.
So we will do it.
Before doing this, it will be nice to initialise
the handle, so we do it like this, it will prevent
to have a bug later.
dice : = OrderedCollection new.
Recategorize.
And now, I must be able to run my
test, which will crash. Ok, very well.
I create Add die.
Adding.
It says: "You should implement this method."
Yes, it makes sense.
I write "Dice add aDie".
Ok, very good. My test won't still
work because I still don't have defined the diceNumber
method, let's do it. Yes, diceNumber,
we will create it, in accessing this time.
And diceNumber, what will it do?
It must return
dice size. I compile again, proceed.
And my test should be green so, the tests
are green and I save.
"With addDie and test".
We could improve the test because here
it checks that we add 2 numbers, I'd like
to check that when
we add twice the same die we don't lose it.
I write "TestAddingTwiceTheSame DieisOK".
Here what do I do?
I add 6 and 6 and I want to get 2
I do this I run my test, it's green, super.
Now, it will be nice to be able
to define what it is to do
add 2 dice. But before this, let's do something.
If you look, what I don't like,
when I inspect this for example,
if I do "Inspect" here, I don't see
the dice values and it's not practical to debug.
In the debugger, we don't see this.
So before going on, I want to improve
this. I'm going to add a method
in the Printing protocol. The "PrintOn" method
is defined on all the objects of the system and
it will convert an object to a
textual representation and
pass a stream. We will only precise
the representation we want inside it.
If I do this, I've done nothing in fact.
If I do super PrintOn, in fact I've done nothing.
Now I will do
"aStreamnextPut", so I will put characters in the
stream, but what will I put first?
I will write a parenthesis with a space, maybe it
will be nicer, a parenthesis.
Then I will consider faces and convert them
in numbers, in strings, and concatenate all this
with a closing parenthesis.
If I do this... I closed the debugger, so I open it again.
I have the debugger. Now I have a 6-sided die
and a 10-sided die. So it is much
nicer, you will see, if we encounter bugs, it will help.
So here I didn't do anything special, my tests are running.
I save again, it doesn't cost much, "With printing".
We write "with die printOn".
All right. Now we create the test, we won't
do it, we will go directly there.
We select "add protocol", "roll", "operations".
So,
there are several ways to define this.
I propose you one, this is not the nicest but
at least it is probably the clearest for you.
There is a compact way, I could
do it in one line, but using iterators
like "Injected to", here I'm gonna use a loop.
So what do I do?
I take a value that I initialize to zero.
Then I do a loop on all the dice, and
for each loop step I get a die, and what am
I going to do with this die?
I ask it to get a die roll and to add the result to
my variable.
Nothing very special but at least it is very very explicit.
Now if I do "Inspect" and there
"Roll", 5, it doesn't prove it is working.
Let's try once more.
11. Ok, it's working.
We are gonna try to write a test, there is
no reason, so we do "Test", I want to see this one
and I call it rolling.
So how do we have to do to test this?
It has to be between one and the maximum of the number of dice.
So we will do this.
We
could define a method doing this.
Let's create a method defining the maximum.
"Operation maxValue".
What is maxValue ?
It is very close to this.
Here instead of doing roll,
I will ask for the faces.
Let's check. If I do "Inspect", there
I do "maxValue", "16".
Yes, it's right 10 and 6.
So we write a test for
"maxValue". So I have this, I do "maxValue equal
16".
So here you see, I could have coded something
very dirty in my test, but finally, it is better
to create a method in the class and to use it.
So now, we can test the roll method
works well. Let's do "roll",
and say it must be comprised
between... "Roll between 1 and
h maxValue.
If I do this, ok it works.
This is not very statistical, so here we could do
something like 1 000 timesRepeat.
Ok. And there,
we have our 1000 tests.
Now we save. All right.
We save once more. "save" added maxValue
and roll with tests.
We've almost finished,
what we want to express now, is
instead of having "die faces 6",
I'd like to have "1 D6".
And
what you see at the end is that it means "send
the message 6 to a small integer".
So we go and look at the integer class.
What we are going to do is
to define a class extension.
What is a class extension?
I'm gonna package my methods with the same name as my package.
So you will see, what do I do? I add a
protocol, I put *, it must start with *dice which is
my package's name, automatically this is put in grey, and
it means the method will be packaged
at the same time as this package. So let's do it.
Let's imagine we do... What is D6?
A D6... I have to think a little about it...
We first create a handle because it could
be 2 D6 finally.
So "handle", we do "diceHandle
new",
ok, so I have created my thing.
Now for each receiver, I will do
"self", this is my integer, "timesRepeat".
We will have really used a lot the timesRepeat, it's rare.
"TimesRepeat handle addDie", of what?
Of "die faces". And there,
we know it's 6.
And indeed, it would maybe be good to return the handle.
So does it work?
We're going to test like this and we write a test.
But if I do 2 D6, Inspect, look,
I do have 2 D6. So that's cool.
Let's write the test.
We will categorize those tests after all.
We write "testNewSyntax".
Here for the moment we only have D6, we will generalize later.
We want to do exactly the same thing
than this, so we will have an handle, let's say 2 D6.
And there, we do "selfAssert".
What could we test?
That diceNumber equals 2
for instance. So you've noticed sometimes I use
diceHandle, I could have also used = 2 here.
In general, it is nicer to use assert equal
because like this, when there is an error, the system
says: "I've received this and got this value instead of..."
If I write =, it will say: "I've got a wrong expression."
Here for the final user, who is yourself,
as a developer, it is better to use
assert equals because it will say: "I've received 3 whereas
I was expecting 2", for instance.
So here, I do this.
Ok, it works.
Could we have a smarter
version of this test?
For the moment, it suits us.
You see that now in
the package, I have an extension called D6.
We will generalize this with
"aNumberOfFaces"
So "aNumberOfFaces", we put it there.
And we rewrite D6 because it would be better.
We write D6 like this. We do
returns self D6.
We do all the other ones.
We do 4,
2. It's more a coin than a die, but 2,
10 and 20.
You've understood the principle.
So let's run the tests, as we've changed the implementation.
2 D6... it means it works.
So let's save.
What is there still to do?
In fact, we have still to be able to add the handles.
What tests do I want?
For instance, I want to be sure if I do
"addingHandles",
(I can use the new syntax,
so it's nice)
I want to test that if
I write 2 D20 + 3 D5
or 3 D6 instead (don't start to complicate
things). How much should I get ?
diceNumber should be equal to 5.
So here you see that we have to define the + operator.
In Pharo + isn't an operator, it's just a message.
So we define a message on the DiceHandle class.
We write +.
So "aDiceHandle".
Now we can wonder if
we modify the receiver or either if we use
a functional approach.
I prefer to use a functional approach
in which we create a new handle.
So I'm gonna create a new handle, I write
"handle self class new".
Here I avoided to write diceHandle and later there
will be a lesson explaining why.
I prefer, it's closer.
In general you don't hard-code the classes' name.
You will see it in week 7 or something like this,
there is a complete explanation.
If I do "self dice do", I
iterate on my dice and I add them in handle.
So I do "handle addDie each",
and I do the same... Here I don't
need self and in fact I don't know
the message, that's what it was telling me, and
it makes me notice that, indeed, I haven't defined it
and it hasn't worked for "diceHandle", but
no matter, let's compile first and we'll fix it later.
So here, what does it mean?
It means it lacks an accessor, dice.
So we add dice here, dice returns
the collection of my dice.
Now I'm gonna test, see if my test is ok.
My test is ok, it's super, it means I have
almost finished, I save, "with handles
additions". Ok, all right.
It means now we can write
2 D4 and we
can do "Roll", and it returns a number.
Now you are ready to play "Dungeons and Dragons".
What you have to know: we defined
our methods, we defined our tests, we run
them, we extended a system class,
the integer class, with extensions linked to our
package, which will only be visible when our package will be loaded.
We also overloaded operators, but
in fact we only defined a new + message, because
in Pharo the addition is just another message,
this enabled us to express quite easily a nice DSL.
So now it's your turn to code!