Ball movement design0:00
So here's how this should work. Once the countdown reaches zero, the ball should appear on one side or the other. We'll choose that side randomly. And then it'll move to a random location on the other side. And once it hits that side, it'll move to a random location on the other side. That's the general idea. Okay, so in our ball loopable, we are going to need a couple of things based on what we just said. We need a public int y. This is where it's heading to next.
Ball properties and boundaries0:29
We need a public int y. This is where it's heading to next. We need a public int direction. And this will either be 1 if it's moving to the right or negative 1 if it's moving to the left. And we're going to want a public animatable x. So this x will be animated as it goes from the left to the right or the right to the left. So let's set some of this up. public function construct.
So let's set some of this up. public function construct(). And we're actually going to need the prong app here because we're going to need to know about the gameHeight and the gameWidth. So let's actually put that in. So ball, we're actually going to need that. Cool. Okay, so we need to know about our limits. So we should actually add two more properties here. I'm going to say public int maxY and public int maxX.
So we should actually add two more properties here. I'm going to say public int maxY and public int maxX. So these are the maximum up and down and the maximum left and right that we are allowed to move. So this maxY is equal to this->promptGameWidth minus 1. And that's just because we need to account for the actual size of the ball, which is one character wide. Also this should be a protected property. And so maxX is actually going to be the gameHeight minus 1. So those are our boundaries.
And so maxX is actually going to be the game height minus 1. So those are our boundaries. That works. Okay, let's figure out which side the ball is going to start on. So we'll say xStart is equal to, and we'll just say collect(0) in this maxX and choose one at random. Great. Let's set up our animatable value. This x equals animatable from value, whatever we chose as the xStart, our lower limit is going to be 0 and our upper limit is going to be this maxX.
This x equals animatable from value, whatever we chose as the x start, our lower limit is going to be zero and our upper limit is going to be this maxX. And then we're immediately going to say whatever this x start is, toggle it to the other one so that we have animation from the jump. Okay, let's figure out a starting y position. Where is this going to start vertically? This y is going to be equal to random, a number from this to this maxY. So from zero to our maxY value, that's going to be the starting y position. So we basically just set up the parameters of how far the ball can go on the screen, and then also some starting x and y values for the ball.
Animate X and steps2:54
So we basically just set up the parameters of how far the ball can go on the screen, and then also some starting x and y values for the ball. So how do we animate this? This x, when done animating, when it's done animating the ball along the x axis, we're going to fire this callback. And all we're going to say right now is this x toggle. So when you're done animating the x axis, just toggle back to the other values. So that'll just make the ball go back and forth and back and forth and back and forth. So the tricky bit of this is if we're starting up over here and then we want to get down over here, we need to figure out the frames that get us from this corner down to this.
So the tricky bit of this is if we're starting up over here and then we want to get down over here, we need to figure out the frames that get us from this corner down to this corner. Let's figure that out. So I think the best way to do this is to create an array of y coordinates that we need to hit. So let's go up here and create a protected array steps, and we'll just say it's a blank array. Let's go ahead and create a public method start. And what this will calculate is the next destination y that we are going to be animating towards.
Let's go ahead and create a public method start. And what this will calculate is the next destination y that we are going to be animating towards. So nextY is equal to rand(0, $maxY). So it's the same thing we did here, and we're just saying the nextY we're going to hit is a random number between zero and $maxY. We'll say $steps equals, and let's just extract this out because I think it's going to be a little complicated to do inline, getSteps($nextY). Let's figure out how we get each of the steps along the way. function getSteps(int $nextY). And this will return an array.
method function getSteps int nextY. And this will return an array. If we just wanted to get from the current y to the next y and we want that as an array, well that's not hard at all. We could just say steps equals range($this->y, $nextY). And this will give us a range of all of the numbers between these two as an array. That's great. What if you're just animating from say this spot to this spot? You're only going up one, but you have to go across all of this way to do that. So in order to do that, you would maybe go like this straight across, and then here you
You're only going up one, but you have to go across all of this way to do that. So in order to do that, you would maybe go like this straight across, and then here you would bump up one and go across. So we need to make sure that the steps array is equal to the maxX in count. So let's fill in anything that might be missing along the way. So we can say while count steps is less than this maxX. Let's just keep appending from the current index to the steps so that we keep getting steps that are in the right range appended onto our steps array. This will be clearer in a moment. So let's do steps equals steps at the current index.
This will be clearer in a moment. So let's do steps equals steps at the current index. And then we'll just increase this i. So now we have sort of an oddly ordered steps array, but all of the values are in the right range. Well, let's get this in the right order. So we can just say sort steps. So now we have the right number of steps, and now they're in an ascending order. But now we need to say, well, are we going up or are we going down? Because that will determine the actual order of the steps.
But now we need to say, well, are we going up or are we going down? Because that will determine the actual order of the steps. So if nextY is less than thisY, we actually want to reverse the steps, return array_reverse(steps), because we want them in the opposite order. Otherwise, we can just go ahead and safely return the steps. So now we have a series of steps we can take on the y-axis in order to move the ball to its next destination. Okay, next let's set the direction. This direction is equal to, well, if thisXCurrent is equal to zero, this means it's on the left-hand side, so we're moving right.
This direction is equal to, well, if this current is equal to zero, this means it's on the left-hand side, so we're moving right. So it's going to be one. Otherwise, it'll be negative one. And now, every time we're done animating, we should actually call this method to actually start the next ballMove. Finally, on every tick, we want to take the next step and set it to y, so we know what the current y is. So we can check if count(this.steps) greater than zero, set this y equal to, and just grab the next one off the steps.
So we can check if count this steps greater than zero, set this y equal to, and just grab the next one off the steps. So this is kind of a lot, but what we're basically doing is we're moving the x back and forth. We're calculating the steps and setting y to the current step as we tick. And so this will give us, between the current x value and the current y value, the positioning of the ball within the grid. In our Application class, we should probably start the ball when we start the game. So when we come down here, after we register the loopable, we should say, this ball start, so that we kick off the process. Let's go into our renderer.
Render the ball8:04
so that we kick off the process. Let's go into our renderer. So here, this center will actually change based on whether we're in countdown mode or whether we have a ball. So if prompt countdown > zero, we are going to show the countdown. Otherwise, let's show the ball. Okay, let's work on this ball method. Go ahead and copy this, change it to ball. Let's focus on the x position of the ball. Let's focus on the actual line where the ball is actually going to be rendered.
Let's focus on the x position of the ball. Let's focus on the actual line where the ball is actually going to be rendered. Not the blank lines above, but the actual line that the ball is going to be rendered. So the ball line is going to be equal to, and this is pretty straightforward, string_repeat a space for whatever the prompt ball x current value is. So we're just going to make a bunch of spaces based on what the current value is. Then we're going to concatenate a little cyan ball, and then we're going to fill up the rest of the line by string_repeat spaces based on whatever is left. string_repeat, max, prompt, game, width, minus, prompt, ball, x, current, minus one to account for the ball, zero.
String repeat, max, prompt, game, width, minus, prompt, ball, x, current, minus one to account for the ball, zero. Okay, so now we have a line that represents our ball. A bunch of spaces, the ball, more spaces to fill out the rest of the line and make sure that we are always rendering the same amount of width on every single line. So now let's actually fill in the y position. So we can say return, collect, array, fill, zero, prompt, ball, whatever the y current value is, just add a bunch of spaces above it, and we'll just push the ball line. Okay, let's see what we've got here. Whoa, okay, we're getting some very wacky behavior and the ball is not going, okay,
Okay, let's see what we've got here. Whoa, okay, we're getting some very wacky behavior and the ball is not going, okay, I think I know what I did here. Yep, okay, so our y is actually gameHeight, and this is gameWidth. I reversed these two values. So let's try this again. Three, two, one, and cool, we are moving the ball across the screen. It's going a little slowly, so we can fix that. I'm also noticing that the gameHeight is not consistent, so no matter where the ball is or where the paddles are, it should always be the same height.
I'm also noticing that the game height is not consistent, so no matter where the ball is or where the paddles are, it should always be the same height. So let's fix that first. So back in our renderer, we can say this pad vertically, and this will push new lines onto a collection based on the height that we are trying to get to. So say this lines from columns and prompt gameHeight. Make sure that it's always at the gameHeight. So that should fix that. Back in our app class, let's just speed this up a little bit. Let's go down to here, and we'll say, instead of, it defaults to 50,000, let's go 25,000.
Back in our app class, let's just speed this up a little bit. Let's go down to here, and we'll say, instead of, it defaults to 50,000, let's go 25,000. Let's check this out. Okay, much better speed. We now have a consistent height, and it looks like our ball is working. This is looking pretty good. It's not picking a lot of super random values. Let's see if it changes. Okay. Oh, yep, there we go.
Detect hits and winner11:28
Okay. Oh, yep, there we go. Now we're marching all the way up there. Okay, cool. So this is working. We have a functional ball, and we can move the paddles while the ball is going. And so now let's figure out how we know if we've hit the paddle and determine a winner. Okay, back in our Application class, let's add a method to determine the winner. So we'll come down here, protected function, determineWinner. And so we'll set the winner equal to, and we have to figure out who we're evaluating.
So we'll come down here, protected function determineWinner(). And so we'll set the winner equal to, and we have to figure out who we're evaluating might have won. So let's match this ball x current, and if it's zero, we're going to say this getWinner(), this playerOne, and playerTwo. So we're saying check against the playerOne paddle, and if they didn't hit it, playerTwo is the winner. And we can say default, we're going to check against the computer and playerOne will be the winner. Cool.
the winner. Cool. Let's figure out what getWinner actually looks like. So protected function, getWinner, we have a paddle as the first argument, and the winnerNumber as the second argument. So let's get the y coordinates of the paddleStart and the paddleEnd. So how high the paddle is and where the ball is relative to that. paddleStart is equal to playerValueCurrent, that's the top of the paddle. paddleEnd is equal to playerValueCurrent plus the playerHeight. So that's the start and end y coordinates of the paddle.
Paddle end is equal to player value current plus the player height. So that's the start and end y coordinates of the paddle. So isHittingPaddle is equal to this ball y greater or equal to paddleStart and this ball y is less than or equal to paddleEnd. So return isHittingPaddle, nobody won this round, or return the winner number. Okay, so we're calculating the y value, we're saying is the ball between that value, and if it is, then we are okay, they hit the ball, otherwise return the winner number. Now, if the winner is not equal to null, well, this state is now equal to winner, and we should have a property and we'll set this up, this winner is equal to winner, so the renderer knows who to say won.
should have a property and we'll set this up, this winner is equal to winner, so the renderer knows who to say won. Let's set this up. Quick int winner equals null by default. Great, so when do we need to determine this winner? Well, we need to determine it at the moment that the direction changes of the ball. So we have a couple things to add to our ball loopable. So it seems like we're going to have to add callbacks to the direction change. So let's add the ability to add multiple callbacks to when the direction changes. Added array directionChangeCallbacks is equal to an empty array.
So let's add the ability to add multiple callbacks to when the direction changes. Added array directionChangeCallbacks is equal to an empty array. We also may want to know how many times the direction has changed, so let's do public int directionChangeCount equals zero. Now let's come down here and add the ability to add a callback. So public function onDirectionChange, we have a callable callback, and we'll say $this->directionChangeCallbacks equals $callback. Now let's come down here, when the direction changes, we'll say foreach $this->directionChangeCallbacks as callback, and we'll just do $callback. Great, so whenever the direction changes, we'll fire off a callback.
change callbacks as callback, and we'll just do callback. Great, so whenever the direction changes, we'll fire off a callback. Let's register determining the winner as a callback for this. So after we register our loopables, let's say this ball on direction change, and we'll say this determine winner. Now one thing we have to add is that if this ball direction change count equals zero, well we haven't actually changed direction yet, so don't determine a winner. Let's just return there. We only want to do this after we've changed direction at least once. Okay, in our renderer, let's do a simple representation of this winning state just so we can see if
We only want to do this after we've changed direction at least once. Okay, in our renderer, let's do a simple representation of this winning state just so we can see if we hit it. So let's add winner, and we'll say renderWinner prompt. We can go ahead and grab this and say renderWinner. The winner is equal to promptWinner is equal to 1, well then you won, otherwise the computer won. So let's print that big on the screen. winnerTitle is equal to this bigText winner 1. Let's just center that around the screen.
Winner title is equal to this big text winner one. Let's just center that around the screen. This center title, this width, this height, each this line. And return this. Let's see if this worked. Our ball is off, and if we hit it, it should be good. That one was good. And this should be fine too, but let's have it actually lose, and it's still working. Let's see here. Oh, we didn't increase this.
Let's see here. Oh, we didn't increase this. This directionChangeCount, we have to increase that because otherwise we'll never determine the winner. Okay, let's try it again. Okay, let's actually get the first one. Let's see what happens here. Now the computer's not moving yet. We'll fix that in a moment. Let's purposely lose, and the computer won.
Computer paddle AI17:29
We'll fix that in a moment. Let's purposely lose, and the computer won. Wow, we did it. Okay, great. Let's make the computer move because otherwise this is a very boring game. Okay, so we already have a way of registering callbacks based on the direction change, so let's actually add another one here. And we'll say calculateComputerPosition, and we can go ahead and just implement this protected function, calculateComputerPosition. Okay, so first let's check if it's even heading in the computer's direction, because if it's
protected function calculateComputerPosition(). Okay, so first let's check if it's even heading in the computer's direction, because if it's not, we don't care about it. If this ballDirection is not equal to 1, then just return. We don't care about this one. So at this point, the ball is heading in the computer's direction. Let's get the destination Y coordinate. So nextY is going to be equal to collect($ball->steps)->last(). So the last step is actually going to be the nextY destination. Now this is red, and I think that means because I made this a protected property, but it needs
So the last step is actually going to be the next $y destination. Now this is red, and I think that means because I made this a protected property, but it needs to be public. And yep, that's right. Okay. So we have access to that now. Okay, so we're going to implement a very simple and not very nuanced lottery system. So we'll say $chances are equal to collect([1, 1, 1, 1, 0]). So we want the computer to have an 80% chance of getting it right and a 20% chance of getting it wrong.
So we want the computer to have an 80% chance of getting it right and a 20% chance of getting it wrong. And this should be chances, not changes. So we'll say if chances random equals 0, we want the computer to miss. Next Y minus equals this computer.height plus 1. So we want it to be off by at least 1 of the computer's height. Otherwise, we're going to say this computer value and animate it to the next Y. Great. So we'll just adjust Y to be off of the paddle if it's the 0 value. And either way, we're going to animate it to the next Y value.
So we'll just adjust Y to be off of the paddle if it's the 0 value. And either way, we're going to animate it to the next Y value. Let's see if this works. Okay. So we saw the computer immediately animate. Let's see. So it hit that one. Let's hit ours. Okay, great. So it seems to be doing pretty well, and it probably will continue to do pretty well.
Okay, great. So it seems to be doing pretty well, and it probably will continue to do pretty well. Let's see if it misses in the next one. This looks good too. Let's give it one more. Let's make it purposely miss for a moment and just make sure that our logic is sound. So we'll hit ours first, and then it should miss, and it does, and we won. Okay, great. This is looking cool. We can re-establish that logic.
Hotkeys and restart20:21
This is looking cool. We can re-establish that logic. Great. So we've calculated the computer position. We're determining winners. I think we're in a pretty good place here. I think the last thing we want to do is add some hotkeys to the screen, and we want to give the User a chance to restart the game when the game is over. So first let's handle some hotkeys. First thing we want to do is we want to use Draw's hotkeys.
So first let's handle some hotkeys. First thing we want to do is we want to use Draw's hotkeys. And then back in our RenderGame method, once we get the box lines, let's add some hotkeys to it. So this hotkey, move paddle, and we can say Q is for quit. And then what we want to do is we want to tell them, well, you're actually player one. You're the red paddle. So hotkeys equals collect, and we'll say this bold, this red. You are player one. So we have some correlation between the red of the paddle and the red of the instructions.
You are player one. So we have some correlation between the red of the paddle and the red of the instructions, and this is supposed to be a method, not that. And then we can say implode, PHP end of line, this hotkeys. And so we're making just sort of like one line out of this. implode, string, repeat, space, four. So this is our hotkeys plus little player instruction line. So let's add that. hotkeyLines is equal to this, center, horizontally, hotkeys, this, width. And then we want to push these lines onto our existing lines.
Hotkey lines is equal to this, center, horizontally, hotkeys, this, width. And then we want to push these lines onto our existing lines. Lines, push, hotkey, lines. So this should give us what we want here. Let's check it out. Yes, looks good. You are player one, move paddle, quit. This is good. The User now knows what is going on here. Great.
The user now knows what is going on here. Great. The last thing is we need to give the user the ability to restart the game once the game is over. So first of all, we need to figure out how to break this loop. Well, how do we break this loop? If $winner is not equal null when we're in the loop, return false. This will break us out of the loop. Now let's go ahead and grab this because we're going to reuse this right now. And so we want to clear any existing listeners.
Now let's go ahead and grab this because we're going to reuse this right now. And so we want to clear any existing listeners. We want to listen for quit. And we also want to listen for R. On R, this, start, game. And that should restart our game and we want to listen now. So we want to actively listen for this right now and pause execution of the renderer. Finally, let's tell them that they can do that. Back in our renderer, in the renderWinner function, we are going to say title, push, so push a blank line, and then we'll say title, push, and we'll say press this bold, this cyan, Q, to quit, or this bold, this cyan, R, to restart.
so push a blank line, and then we'll say title, push, and we'll say press this bold, this cyan, Q, to quit, or this bold, this cyan, R, to restart. So this should just push a new line and then this line onto our winner message. And this should do it. Let's see how it goes. Okay, we've got our title screen. We press enter. We get into the game. The ball starts moving. We can play a little bit.
The ball starts moving. We can play a little bit. The computer will move on its own. And let's see if we lose. Okay, computer won. We have a nice little line. Press Q to quit or R to restart. If I press R, we're back into countdown mode and we're back. Oh, and the ball didn't start. Let's figure that out.
Oh, and the ball didn't start. Let's figure that out. Okay, the ball didn't start because it was a little confused about some state stuff. So we need to actually set winner equal to null when we start the game again. So this startGame needs to really like reset everything back to baseline when we restart the game. So let's try it again. Okay, title screen. Countdown. And we've got a ball going and let's just purposely lose right away.
Countdown. And we've got a ball going and let's just purposely lose right away. And the computer won. We press R, restarts the countdown, and our ball's moving. Okay, great. So now we should be able to play and I think we just made Pong in the terminal. This is so rad. I'm so into this. Okay, so where can you go from here? You could make this more complicated.
Okay, so where can you go from here? You could make this more complicated. You could say every time it changes direction, like every four times, you could increase the speed of the ball or every four times you could shrink the height of the paddle. These are all incredibly easy things to do, set up the way that we've set this up. You can make fully interactive games in the console using prompts, which is so cool.
