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.