351 lines
22 KiB
Plaintext
351 lines
22 KiB
Plaintext
|
|
Episode: 3582
|
||
|
|
Title: HPR3582: Rolling a new character
|
||
|
|
Source: https://hub.hackerpublicradio.org/ccdn.php?filename=/eps/hpr3582/hpr3582.mp3
|
||
|
|
Transcribed: 2025-10-25 01:45:55
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
This is Hacker Public Radio Episode 3582 for Tuesday the 26th of April 2022.
|
||
|
|
Today's show is entitled Rolling a New Character.
|
||
|
|
It is part of the series' Haskell.
|
||
|
|
It is hosted by Tuku Turo Toe and is about 30 minutes long.
|
||
|
|
It carries a clean flag.
|
||
|
|
The summary is Tuchitou continues writing an example Haskell game this time rolling a
|
||
|
|
new character.
|
||
|
|
Hello, I'm Tula Turtou and you are listening to the Hacker Public Radio.
|
||
|
|
This time we'll continue writing our small game in Haskell and we are going to create a random
|
||
|
|
random charakter.
|
||
|
|
So before we go, let's have a quick peek at some places in the code.
|
||
|
|
So we have the main.hs file that contains the main module definition that was generated
|
||
|
|
automatically by the stack when we started this project.
|
||
|
|
And in the end of that main function test the call to the run function, which is defined
|
||
|
|
in the run.hs file.
|
||
|
|
And touch the place where we can see the overflow of our programming plans.
|
||
|
|
If you want to think that that is our main thing that our program does.
|
||
|
|
I copied the source code into the show notes if you want to have a look.
|
||
|
|
But basically there's a first we are using the show main menu to ask the user if they
|
||
|
|
want to.
|
||
|
|
It will display the main menu and ask user if they want to start a new game or quit.
|
||
|
|
And depending on the choice, there's a case analysis, case choice of.
|
||
|
|
And if there's a exit game, then we'll just return from the run function and then the
|
||
|
|
whole program to end.
|
||
|
|
Or we go to the branch where we create a new character and select the equipment and
|
||
|
|
start playing.
|
||
|
|
And in the end display the game over screen.
|
||
|
|
So the another interesting module is the types that you can find how player items, monsters
|
||
|
|
and such.
|
||
|
|
I'm not copying it in the show notes.
|
||
|
|
But if you want to have a look at it, it's at the GitHub, sorry, code book repository.
|
||
|
|
There's a link in the show notes to there.
|
||
|
|
And be mindful that you look at the 0100 tag because there's a newer code than what
|
||
|
|
I'm talking about now.
|
||
|
|
And that's just slightly slightly different test being some reorganization done and some
|
||
|
|
extra things there that I may get get into the future, but at this time.
|
||
|
|
So our run function basically as it has a bunch of log statements to tell what the program
|
||
|
|
is currently doing.
|
||
|
|
Those are using the real, they come from that.
|
||
|
|
And then there's lines like player left arrow, list IO, ever run IO, role new character.
|
||
|
|
These are what roles are new character.
|
||
|
|
And then there's that ever run IO that's a clue to us that we are dealing with the random
|
||
|
|
number generators.
|
||
|
|
So it takes the global random number generator and supplies that do the role new character.
|
||
|
|
Because in the Haskell, I will get into the random numbers bit more to the later, but
|
||
|
|
in the Haskell state is usually immutable.
|
||
|
|
So if you call a function and give it to a random number generator, you're always going
|
||
|
|
to get the same result unless you give it a different random number generator.
|
||
|
|
So that ever run IO takes the global random number generator and threads through the calls
|
||
|
|
inside of the role new character.
|
||
|
|
And the left IO thing is that via in a RIO, monot now, the signature of the run function
|
||
|
|
is RIO app parenthesis, that means that it doesn't return a value.
|
||
|
|
It has a app, has a configuration, and it uses RIO monot.
|
||
|
|
So that allows us to access that configuration, we are not using it in this program, but
|
||
|
|
it's there if we wanted to use it.
|
||
|
|
And it allows us to do debugging by outputting our log statements that we are using.
|
||
|
|
And it allows us to print on the screen, but if we are using our things that are using
|
||
|
|
IO directly, we have to use lift IO.
|
||
|
|
It's a little bit, I'm not probably the best, that's not probably the clearest explanation,
|
||
|
|
but it's enough for us.
|
||
|
|
So if you see lift IO, it means that we are using things that are designed for the IO
|
||
|
|
monot inside of RIO monot.
|
||
|
|
Anyway, so what our function do?
|
||
|
|
So there's the choice left arrow, so main menu, let's choose the main menu, but every
|
||
|
|
value it returns is placed into choice.
|
||
|
|
Then there's a case choice of, that's a case analysis, there's two branches, start
|
||
|
|
new game or exit game.
|
||
|
|
And there's the player left arrow, lift IO, L run IO, roll new character, that rolls
|
||
|
|
out new character.
|
||
|
|
And those are dollar sign, that's there to instruct that, that haskel to evaluate that
|
||
|
|
eva run IO roll new character first, and whatever that returns, supply that as a parameter
|
||
|
|
to the list IO.
|
||
|
|
If we didn't have it that day, the haskel will break this line that call lift IO function
|
||
|
|
and supply two parameters, eva run IO and roll new character, and that's of course
|
||
|
|
doesn't work.
|
||
|
|
You could replace that dollar sign with the parenthesis around eva run IO roll new character.
|
||
|
|
The end result would be the same, it's just a different way of writing it.
|
||
|
|
This play new character, player, that just displays our freshly created character, here
|
||
|
|
left arrow select starting gear, player, player, that creates our, that uses our equipment
|
||
|
|
that possibly get generated in the rolling new character, we are actually not using that.
|
||
|
|
And it displays a menu where the player can choose the equipment.
|
||
|
|
And the result of that is placed into gear variable.
|
||
|
|
Left arrow, lift IO, eva run IO start game player gear, this starts a new game, it
|
||
|
|
suffers the deck and prepares things for the game to start.
|
||
|
|
And the result of that is placed into the game variable.
|
||
|
|
This variable now contains a type or value that represents our game, it has the deck and
|
||
|
|
the current card and the player and the player gear and all that stuff.
|
||
|
|
Finished game left arrow, play game game.
|
||
|
|
So this plays the game until we are done and the end result is placed into the finished
|
||
|
|
game.
|
||
|
|
And this play game over finished game, this plays the game over screen.
|
||
|
|
So we are about input and output, so one of the things that I really like about the
|
||
|
|
Haskell is that you have ability, I mean your force basically to state which functions
|
||
|
|
have access to the input and output, printing on the screen, doing network calls, accessing
|
||
|
|
databases, accessing a functional disk and which functions are pure in a sense that
|
||
|
|
they are always just data in, data out.
|
||
|
|
It's guaranteed that when you call a function that you will always get the same parameters
|
||
|
|
you are always going to get the same result.
|
||
|
|
So 1 plus 1 is always 2, it doesn't execute any of the 3, 1 plus 1 also doesn't print
|
||
|
|
on the screen, it doesn't access the hard drive or anything, it just takes those 2 values
|
||
|
|
and produces a new value.
|
||
|
|
So this, but that makes a, for example, testing, very pleasant because the more you have
|
||
|
|
those pure functions, the easier it is test because you can just write examples or proper
|
||
|
|
to paste the testing, oh, whatever you like.
|
||
|
|
And it's always like data in, data out, you can always, you can even go through the code
|
||
|
|
by hand and figure out that if I give these values the results will be this and then write
|
||
|
|
your test like that.
|
||
|
|
As soon as you start mixing in IO, things get really tricky because if you have done any,
|
||
|
|
for example, unit testing or integration testing, what any automated testing you know that
|
||
|
|
as soon as the database involved or files involved, the testing is a little bit tricky.
|
||
|
|
The same thing is with the reasoning about the code, it's easy when you can just look
|
||
|
|
at the code and you know that it's values in, values out, nothing is going to change
|
||
|
|
somewhere outside of the function.
|
||
|
|
Okay, showing the main menu, that show main menu function has a signature, real, real
|
||
|
|
IO up main menu choice.
|
||
|
|
So it doesn't take any parameters, it uses the RIO, it has the app as a configuration and
|
||
|
|
it returns main menu choice.
|
||
|
|
And it has a lot of those lift IO statements, they are just to make sure, they just make
|
||
|
|
that the put SDRLN, that has been designed to work with the IO, that it works inside
|
||
|
|
of this RIO monoc.
|
||
|
|
So we are just using the put SDRLN to print out nice menu with two choices and then we
|
||
|
|
are calling main menu input function.
|
||
|
|
And that one has a signature of RIO app main menu choice, the same signature and this
|
||
|
|
uses the getline to read a line from the user, so user can type, feel or more characters
|
||
|
|
and hit the enter and that whatever they typed is placed into the I.
|
||
|
|
I left a RIO getline, so we get one line and place it very descriptively named parable
|
||
|
|
I, that is terrible name, but apparently I choose to use terrible name there.
|
||
|
|
And then we are doing the case analysis, case I of one, return start new game, two, return
|
||
|
|
exit game.
|
||
|
|
So if the user and the one, we return the start new game and two, we return to exit game
|
||
|
|
and then test the underscore write arrow, that means that whatever user inputted and didn't
|
||
|
|
match anything previously, we handle in this branch.
|
||
|
|
And then we first do the log debug, we tell that somebody made an incorrect menu choice
|
||
|
|
and show what they, what they typed is this ends into the log.
|
||
|
|
It goes into the SDR stream, usually that is directed to the screen, but when user starts
|
||
|
|
the game, they can use the, let's see if I can remember, I think it was two greater
|
||
|
|
than, and then file name, that directs the SDR stream into the file, so they can target
|
||
|
|
it into the log.dxt for example.
|
||
|
|
Then we instruct the user, please select one or two and then we call main menu input again,
|
||
|
|
the same function we were, so we keep looping, looping, recursing in the inside this function
|
||
|
|
until user enters one or two.
|
||
|
|
And now you might be thinking that what about stuck, doesn't that mean that if user enters
|
||
|
|
incorrect choice plenty of times, then they will be a stuck overflow and the game will
|
||
|
|
crash.
|
||
|
|
That's not the case with the Haskell, I'm not actually quite completely familiar with
|
||
|
|
the technical implementation, but the Haskell doesn't have a stuck in the same sense than
|
||
|
|
in other programming languages, and also the compiler is pretty good at optimizing code.
|
||
|
|
And here the main menu input is the last call of the function, so whatever the main menu
|
||
|
|
input returns is what this function will return.
|
||
|
|
So the Haskell understands that the compiler understands that there's no work to be done
|
||
|
|
with the retuned value inside of this function, so we can just return the value directly.
|
||
|
|
We don't have to keep track how many, but value have been returned from the function and
|
||
|
|
do some extra work.
|
||
|
|
So it's the, it's the last call of the function, so we can just return the value directly.
|
||
|
|
We don't have to calculate the value inside of that function return here and then return
|
||
|
|
from here.
|
||
|
|
We can just return directly from the code function to the whatever place called this function.
|
||
|
|
So rolling on your character, test this, you know, run function test, this player left
|
||
|
|
I will run IO rolling character, that rolls a new character and because test that I will
|
||
|
|
run IO, we know that this deals with the random numbers.
|
||
|
|
If you look at the signature of the rolling character it is a random Gen G, tick arrow,
|
||
|
|
run G player.
|
||
|
|
Okay, this thick arrow means that on the left side there's some constraints.
|
||
|
|
So G, G can be, G is some type that has a type instance of the random gen.
|
||
|
|
Random gen is a type class that defines, in their face basically, there's a set of functions
|
||
|
|
that has to be defined for that type.
|
||
|
|
And this function returns round G player.
|
||
|
|
So whatever G is, we don't care, as long as it has an instance of the random gen.
|
||
|
|
It could be the one that is in the Haskell libraries, or it could be one that you have written,
|
||
|
|
if you have some really, really nifty implementation for the random number generation.
|
||
|
|
So we essentially, we are returning a player, and we are returning a behind the scenes
|
||
|
|
in such this random owner, we are returning the new state of our random number generation.
|
||
|
|
Because remember I said that the venue called Function with the same result,
|
||
|
|
the same parameters, you always go into get the same result unless it's the IO involved.
|
||
|
|
Because then of course you cannot guarantee that the database has the exactly same data
|
||
|
|
or it actually even exists there or something.
|
||
|
|
But even with random number generators, I mean with random numbers, that is true.
|
||
|
|
Same input, same output, even if you have a random numbers numbers.
|
||
|
|
And this might sound a bit contradictory, but it works in the way that you,
|
||
|
|
when you want to generate a random numbers, you can do it either in the IO monar,
|
||
|
|
and then the results depend on the state of the universe basically,
|
||
|
|
or you can do it in a pure function where you have to supply your random number generator.
|
||
|
|
And that random number generator has a state.
|
||
|
|
So given the same random number generator state, you are going to get the same result.
|
||
|
|
And when you ask a random number from that generator, you are going to get a random number
|
||
|
|
and you are going to get a new state for a random number generator.
|
||
|
|
If you ask the same random number generator, new random number,
|
||
|
|
you are going to get the same number back and it's the same state.
|
||
|
|
But if you then ask a new random number using that new state that you get after the first call,
|
||
|
|
you are going to get a different random number.
|
||
|
|
So if you were to do this by hand and you need it, let's say, 10 random numbers,
|
||
|
|
you would have to make 10 calls, give hold of those values that I returned
|
||
|
|
and make sure that every sub-sequent call is using the latest random number generator state
|
||
|
|
that you got from the previous call.
|
||
|
|
It is possible to do this by hand, it's really tedious and really error prone
|
||
|
|
because you are going to end up with the random number generator one,
|
||
|
|
random number generator two and so on.
|
||
|
|
So that's why we are using the run, monor here, which makes sure that when you are making those calls,
|
||
|
|
it will rate that random number generator state behind the scenes,
|
||
|
|
making sure that every call is going to use the latest generator state.
|
||
|
|
So you don't have to do it by hand, you can just say that give me 10 random numbers
|
||
|
|
and it will give those to you and the state of the generator will be moving behind the scenes
|
||
|
|
without you having to worry about it.
|
||
|
|
And when you are returning, like here, we are returning the run, cheap player,
|
||
|
|
we are returning the generated player and behind the scenes,
|
||
|
|
we are also returning that latest state of the random number generator.
|
||
|
|
So if somebody were to use this function in a part of a bigger random computation,
|
||
|
|
they could just call this and they would get back the random player
|
||
|
|
and behind the scenes, they would have the random number generator
|
||
|
|
threading through the future calls.
|
||
|
|
This is pretty nifty thing.
|
||
|
|
So implementation of this roll-new character isn't too complex.
|
||
|
|
So we are doing that STR left, dice three, text, left arrow, dice three,
|
||
|
|
mind, left arrow, dice three, max hope, left arrow, dice four.
|
||
|
|
So we are throwing three or four six other dice and summing them together
|
||
|
|
and putting those values in the STR, text, mind and max HP.
|
||
|
|
So and then we are returning a player where we are setting those values.
|
||
|
|
So the player's strength will be the STR and player's extraity will be the text
|
||
|
|
and player's mind will be the mind and the player's HP will be the max HP
|
||
|
|
and the player's max HP will be the max HP.
|
||
|
|
So we are keeping, we have a separate values for the HP that is the current hit points
|
||
|
|
that represent the player's vitality or life force.
|
||
|
|
And then we have the max HP that represents what's the maximum of that value.
|
||
|
|
Because during the adventure team, HP might go down or go up,
|
||
|
|
but it will never have, it is never allowed to go to the over the next HP.
|
||
|
|
And that dice is, of course, written by us, that has a signature random chain G,
|
||
|
|
thick arrow, natural arrow, round G, natural.
|
||
|
|
So it takes a, it deals with the random numbers.
|
||
|
|
So it has that round G won't, it takes one parameter that is natural
|
||
|
|
and it returns a natural natural is a type
|
||
|
|
for number, natural numbers.
|
||
|
|
So it can be 0, 1, 2, 3, 4, and so on.
|
||
|
|
It cannot be negative.
|
||
|
|
And Haskell cannot, well, you, I think you can write code
|
||
|
|
that it could check that in the run in the compile time.
|
||
|
|
But I don't know, I'm not good enough to do such things.
|
||
|
|
But it will, it will check in the runtime
|
||
|
|
when you create a new natural number that is 0 or more.
|
||
|
|
If you try to create a natural number of minus 1,
|
||
|
|
you're going to get a random exception.
|
||
|
|
So this is the advantage that in here,
|
||
|
|
when we are using the natural number,
|
||
|
|
we can be sure that it is 0 or greater.
|
||
|
|
We don't have to worry about doing checks
|
||
|
|
that what about if it's negative, it cannot be.
|
||
|
|
We don't have to worry about that.
|
||
|
|
But on the other hand, when we are doing calculations
|
||
|
|
with natural numbers, we have to be sure that we think
|
||
|
|
that what is the possible results of this.
|
||
|
|
If you are doing the multiplication or adding up,
|
||
|
|
then no problems.
|
||
|
|
But as soon as we are starting to do subtraction,
|
||
|
|
we have to be careful that we don't subtract subtract
|
||
|
|
bigger number from the smaller number,
|
||
|
|
because that will get us into the negative territory
|
||
|
|
and that will cause a runtime exception.
|
||
|
|
So when working with the natural routes,
|
||
|
|
we always have to check before doing subtraction
|
||
|
|
that we are not going to subtract too much.
|
||
|
|
And if we are going to subtract too much,
|
||
|
|
then we have to think about in the terms of our program logic.
|
||
|
|
Should we throw an exception?
|
||
|
|
Or should we return 0?
|
||
|
|
Or should we return something else?
|
||
|
|
We have to think what it means.
|
||
|
|
Now a program stands that we are trying to subtract too much.
|
||
|
|
But anyway, here we are not going to do,
|
||
|
|
we are not doing subtractions.
|
||
|
|
We are going to generate random numbers.
|
||
|
|
That we are going to do with these line roles.
|
||
|
|
Left arrow get random rs16.
|
||
|
|
OK, so this gives us a random numbers between 1 and 6,
|
||
|
|
inclusive.
|
||
|
|
So 1, 2, 3, 4, 5 and 6.
|
||
|
|
And it will give us an infinite list
|
||
|
|
of random numbers.
|
||
|
|
Like it is really infinite.
|
||
|
|
But because today Husker is written and how Husker operates,
|
||
|
|
that list is when we evaluate it, it's always evaluated only as much as we need.
|
||
|
|
So if we take the first number, then we are going to get the first number.
|
||
|
|
And then we have something called trunk.
|
||
|
|
In other languages, it's similar to promise.
|
||
|
|
It's not quite the same, but it's close enough.
|
||
|
|
So if we take the first element of that list, then we get the first element.
|
||
|
|
And then the rest of the list is there, but it hasn't been computed.
|
||
|
|
If we take the second element, then the second element is computed, but not the rest.
|
||
|
|
So it's an infinite list of random numbers that is evaluated when we need it.
|
||
|
|
So don't try to count how many numbers there are, because that will cause problems.
|
||
|
|
So and then we do the let's roll equals some take from in the call n rolls.
|
||
|
|
So rolls is our infinite list of random numbers.
|
||
|
|
Take is a function that takes our two parameters.
|
||
|
|
It takes the index and the list.
|
||
|
|
And it takes up as many elements from the list as that parameter tells you.
|
||
|
|
And because it uses index and not the natural, we have to use the from integral function
|
||
|
|
to convert our natural into the integral.
|
||
|
|
So take from integral n rolls, takes n elements from that rolls list.
|
||
|
|
So we take n dice rolls, random dice rolls, and then sum, sums them up.
|
||
|
|
And then we return now the roll is in the, so we have to again use that from integral
|
||
|
|
to convert it from the integral into the natural.
|
||
|
|
So this little thing, these three lines is going to roll dice n amount of times, sum them
|
||
|
|
together and return the sum, sum result.
|
||
|
|
And of course the state of the random number generator, but it's going on the behind
|
||
|
|
the scenes.
|
||
|
|
We don't have to worry about that at all.
|
||
|
|
Okay, I could have used here a index as the value of the n, then we would not have to
|
||
|
|
do those two from integral calls.
|
||
|
|
But I wanted to use the natural because it tells to the call pretty plainly that you have
|
||
|
|
to supply zero or greater number when you're calling this function.
|
||
|
|
You cannot put minus one here or some or 1.5 because those don't make sense.
|
||
|
|
You cannot take minus one elements from the list.
|
||
|
|
You cannot throw minus five dice at least in this, this game you cannot do that.
|
||
|
|
Okay, so now we have a basically out for our program and we have a random, random
|
||
|
|
regenerated character.
|
||
|
|
I want, originally I wanted to deal with the carrying up the character too at this
|
||
|
|
episode, but it's already half an hour, I don't, I don't know how, I don't know if I
|
||
|
|
rumbled too much or read the time code, but anyway, next time we are going to have a look
|
||
|
|
how to, how to key up the character, this is a little bit more user, user, user
|
||
|
|
index and required.
|
||
|
|
Here we only have the user chooses one or two to start a new game or quit the game.
|
||
|
|
But in the next episode, they will be a little bit more interactivity, it will be still
|
||
|
|
in the text console because text console is a lot easier to work with than some say
|
||
|
|
graphical user interface and it is a living to write a whole game in the as little complications
|
||
|
|
as possible.
|
||
|
|
I'm sure that the game that we are writing here could be written in a much more clean
|
||
|
|
and much more elegant or much more abstracted way, but that will trigger some, somewhat
|
||
|
|
more efforts from our end and now the goal is just to write a game and get it done.
|
||
|
|
And may be very about making the code pretty and elegant in the later when it's done, but
|
||
|
|
first you want to get it done.
|
||
|
|
And like I said, the game is available at the code book, code book repositories are
|
||
|
|
linked in the show notes and please look at the 01000 tag because in the, the new code
|
||
|
|
in the repository I did some restructuring and some extra things there that hopefully
|
||
|
|
we will get into the later.
|
||
|
|
If you have any questions, comments of earpack, you can reach me by email or you can reach
|
||
|
|
me at the fediverse, where I am to do it at the LG TV or you even better you could write
|
||
|
|
your own, I record your own episode and post it in the HackerPublic Radio at astra.
|
||
|
|
You have been listening to HackerPublic Radio at HackerPublicRadio.org.
|
||
|
|
Today's show was contributed by a HBR listener like yourself.
|
||
|
|
If you ever thought of recording a podcast and click on our contribute link to find out
|
||
|
|
how easy it really is.
|
||
|
|
For HBR, it has been kindly provided by an onsthost.com, the internet archive and our
|
||
|
|
things.net.
|
||
|
|
On the Satellite status, today's show is released under Creative Commons, Attribution 4.0 International
|
||
|
|
License.
|