تماشای این درس نیاز به اشتراک حرفه‌ای دارد.

Persisting game state0:00

Just before we get started, in all the excitement, I actually forgot to change this hard-coded loop here. So let's go ahead and make that dynamic. game.player2.name. There we go. Now we're safe to carry on. Now at the moment, our server knows absolutely nothing about the state of a game. Which essentially means if we were to fill this out and then reload the page, we'd get a blank slate again. There's no way to restore previous state. Which is kind of important if we're going to be dealing with two different browsers across a network. So first of all, let's make sure that the server is able to track the current state of the game. We'll try and keep this as simple as possible. I'm going to add a string to our games table. We'll call it state. And we'll allow it to be nullable, which would

of the game. We'll try and keep this as simple as possible. I'm going to add a string to our games table. We'll call it state. And we'll allow it to be nullable, which would indicate that the game hasn't actually yet started. Okay. Now we could go into our game factory just to make sure we're not going to break anything here. We'll add in state like so. And set that to null out of the box. And then if we go into our game model, I essentially want to treat this as JSON. I want to be able to take the array of numbers, minus one, zero, one, push that into that string column. It gets saved as JSON. And then when we pull it back out, it's converted back into an array. And we carry on as if nothing's happened. So let's implement the cast method. And I'll say that for the state column, I simply want to use Laravel's built in JSON cast. Couldn't

Validating and updating state1:29

And we carry on as if nothing's happened. So let's implement the cast method. And I'll say that for the state column, I simply want to use Laravel's built in JSON cast. Couldn't be easier. I so love Laravel and all the utility it provides. Right. fillable. Let's also add state there to make things nice and simple. And then I think we can jump into the GameController. And we'll want the update method here, right? Because we're going to be updating an existing game. First of all, we'll validate the data coming in. So request, validate. And we'll pass in state, which is required. Should be an array. And we can actually set the size of nine because we know exactly how many squares exist on the board. And then for each item in that state array, we'd expect an integer. And we could use between here because the integer

we know exactly how many squares exist on the board. And then for each item in that state array, we'd expect an integer. And we could use between here because the integer should be between minus one and one. It can only be minus one, zero, or one. Okay. Let's assign that validated data to a variable. And then we should be able to say game.update, passing in the data. And finally, we should be able to return to the games.show route, passing the game variable. Right. Let's update our web.php. And currently we have index, store, and show. Let's go ahead and say that update is also supported. I think that's the backend taken care of. So now we can go to our view component and update the frontend code. In my mind, it makes sense that we perform this server update after we've filled a square, perhaps even before we check for victory.

Syncing state from frontend3:00

and update the frontend code. In my mind, it makes sense that we perform this server update after we've filled a square, perhaps even before we check for victory. So something like router, that's the Inertia router.put. We'll grab the correct route, which would be games.update. And we can pass in props.game.id. And then we want to sync that game state we talked about. So we can set state to, and we should just be able to pass boardState directly. So boardState.value. And I think that should work. Let's test it out. Just before we test in the browser, we'll need to apply our migration changes. So let's run php artisan migrate:fresh, and I'll reseed the database at the same time. And then hopefully in the browser, we can log in again, test at example.com. Let's go to create game. And I'm going to fill out a few of these squares and refresh.

same time. And then hopefully in the browser, we can log in again, test at example.com. Let's go to create game. And I'm going to fill out a few of these squares and refresh the page. And that didn't work. Sometimes I make myself laugh. I've not even updated the view component, have we? Let's do that. So into show.view, here's the board state. And we'd want to say, is there any state already existing inside the prop here? So props.game.state. Or if that's null, that would indicate that the game's not yet started. So we'll use the default that we have here. Okay, back to the browser. Hey, look at that. So you can see that it's actually updated itself with all of the existing squares that we filled out. If I go ahead and fill out another square, and then we reload the page, we get the same state back again. So this is a great first step. But again, it would require two browsers.

Broadcasting moves event4:37

If I go ahead and fill out another square, and then we reload the page, we get the same state back again. So this is a great first step. But again, it would require two browsers to continue to refresh the page. We have the same problem that we were having earlier with waiting for Player 2. So we need to make sure that we broadcast an event that's going to allow Player 2 to automatically receive moves that Player 1 makes, and vice versa. Let's go ahead and create the event. php artisan make:event PlayerMadeMove. I think that makes sense. Nice and declarative. Go ahead, create it. And then we can jump into that event inside our IDE. We'll want to say that it implements ShouldBroadcast. And then let's pass the game into the constructor, public game, $game. That will be now passed down in the event automatically. And when we broadcast, well, remember, we've set up a presence channel.

pass the game into the constructor, public game, game. That will be now passed down in the event automatically. And when we broadcast, well, remember, we've set up a presence channel here. So we want a new presence channel. And it's called games. and then we pass in this game ID. Okay. I think that's everything needed for the actual event. If we jump back into our GameController, after we've updated the database, we could fire this event. But we only want to fire it to the other player. So if Player 1 makes the move, then we want to fire the event to Player 2. And if Player 2 makes the move, we want to fire it to Player 1. Laravel allows this by saying broadcast, and then you pass in the event, which would be a new PlayerMadeMove event, giving the game to the constructor. And then you call toOthers. You chain that on the end, and only other players in the channel will actually receive this event. So cool.

giving the game to the constructor. And then you call toOthers. You chain that on the end, and only other players in the channel will actually receive this event. So cool. Now we just need to get the front end to listen for that event. So we'll chain onto our echo code, the listen hook, and we want the same name, PlayerMadeMove. In the callable here, I'll destructure the event and grab the game from it. And then we should be able to just update the boardState value to be equal to game.state. And that should sync between both browsers at the same time. Okay. Just before we test it out, let's make sure everything's up and running. So we'll need to php artisan reverb:start. And then in a separate tab, at the same time, I want to make sure the queue is running so that as that event is put onto the queue, it's being broadcast out. So php artisan queue:listen. All right. We're good to go. Let's test

time, I want to make sure the queue is running so that as that event is put onto the queue, it's being broadcast out. So php artisan queue:listen. All right. We're good to go. Let's test it in the browser. So hopefully, as I click the square here on one screen, you can see that it automatically updates on the other screen. Let's take a turn as player two. I'll put a nought here. You can see it updates on the left. Let's put a cross in the center. And you can see it updates on the right. We'll put a nought in the far right here. And it updates on the left. And then let's win the game as crossers. I'm going to put one in the bottom right here. And we can see that X has won. Okay. So that didn't work on player two's screen. The X appeared, but we're not checking for a victory state after the event is synced. Just so we're on the same page before we make this change, that homework I asked you to do a couple of episodes ago, this is how I elected

Handling victory on sync7:56

for a victory state after the event is synced. Just so we're on the same page before we make this change, that homework I asked you to do a couple of episodes ago, this is how I elected to do it. So I have this checkForVictory method that I've extracted. And then in here, instead of calling alert, which is what we were doing naively a couple of episodes ago, I have this gameState object that I can update to say whether X has won or crossers have won, whether noughts have won, whether it's a stalemate. And then obviously if it's not, that would mean that the game is currently in progress. So I think what I could do is I could use this checkForVictory function, and I could just call that underneath updating the boardState here. So checkForVictory, and hopefully we can now see that working. So let's play the game again. I'll go top left, and then we should be able to go perhaps in the center. And then why don't we go top middle.

and hopefully we can now see that working. So let's play the game again. I'll go top left, and then we should be able to go perhaps in the center. And then why don't we go top middle and then I'll go to the far right here. Big mistake here because I can now win as crossers, X has won. And you can see now because victory is calculated in both instances, we also get the pop-up on player two screen. Now, one thing that I'd like to do is when I click play again, you'll see that it resets the board state, but that's not actually synced back to player one screen. So they never actually know that we want to play again with them. Let's make sure we fix that one. In my chosen approach, I have a modal here, which when closed calls resetGame, resetGame essentially just sets the board state back to nine zeros. And then we update our gameState object to say that the game is back in progress.

which when closed calls resetGame, resetGame essentially just sets the board state back to nine zeros. And then we update our gameState object to say that the game is back in progress. So I think what I could do is steal this code that updates the server. And then if we head back down here, after we've updated the gameState, I will post that out. So in other words, as far as the server is concerned, the game is now back at nine zeros, it's an empty board. And of course that would fire the playerMadeMove event again, which would check for victory. And when we check for victory, I think all I'd have to do is a nice early return here on the final if statement, and then have a default of saying, look, if none of those if statements are true, it means the game must still be in progress. So we'll make sure to change the state back to gameStates.inProgress. And I think that would basically be a coverall

those if statements are true, it means the game must still be in progress. So we'll make sure to change the state back to gameStates.inProgress. And I think that would basically be a coverall that when you reset the game on one screen for one player, the other player also receives the same updates. Let's check it out. So I'm going to finish this game by putting a X in the bottom right corner. X is one player two sees that X is one as well. They click play again to reset the game. And indeed the board state is reset. And as soon as that event is synced back to player one, their board is also reset and the modal automatically disappears, allowing them to start playing again. And the moves should sync back and forward between the two screens. Now, of course, at the moment you can play every turn of the game, which is not great, right? So I can play both noughts and crosses and there's nothing the other player can do about it, which is not a great.

Enforcing turn-taking rules10:59

at the moment you can play every turn of the game, which is not great, right? So I can play both noughts and crosses and there's nothing the other player can do about it, which is not a great experience. So let's make sure you can actually only play moves. If it's your turn, we already know whether it's crosses turn. Let's make things simple and say that playerOne is always crosses. So we could have a const here called maybe your turn to make it nice and declarative, which would be a computed Boolean. And here we can say, are you playerOne? How can we determine that? We could say if props.game.playerOneID, and we know the currently authenticated user because that's given to us by Inertia. So const page equals usePage. And we'll say props.game.playerOneID is equal to page.props.auth.user.id. Okay. So we know now if you're inside this if statement, you are playerOne. And if that's the case, we could just return is it crosses turn return

one id is equal to page.props.auth.user.id. Okay. So we know now if you're inside this if statement, you are playerOne. And if that's the case, we could just return is it crossesTurn return xTurn.value. Otherwise we know that you are playerTwo and we would just return the opposite return not xTurn.value. So this is now a Boolean that would indicate whether or not it's your turn to go. And I think the simplest way to use it would just be to do an early return inside the fillSquare function. So if not yourTurn.value, then of course we will return early. Let's see if that actually works. So as playerOne, I should hopefully be able to play in the top left square here. And indeed I can put a cross there, no problems. And it sinks to playerTwo screen, but with our new logic in place, yeah, I'm clicking in these squares. You can't see that I'm clicking, but I'm clicking in these squares. Nothing is happening. However, if I go

two screen, but with our new logic in place, yeah, I'm clicking in these squares. You can't see that I'm clicking, but I'm clicking in these squares. Nothing is happening. However, if I go to playerTwo screen and I click say in this top middle, I can place a nought. But again, if I then attempt to play another square, it won't let me, it won't let me until playerOne takes their turn again, at which point it sinks back over and I'm able to carry on. Great. So that logic works without issue. Remember we said that we wanted these little cross and nought indicators to change background color depending on whose turn it was. Let's add that at the same time. I'm going to use class object syntax here. So :class, we'll open up our curly braces and I want to set a class of bg-green-200. I think I'm going to go 200 when we tried 500 a couple of episodes ago, I wasn't all that happy with how that looked. And then we need a Boolean and

and I want to set a class of bg-green. I think I'm going to go 200 when we tried 500 a couple of episodes ago, I wasn't all that happy with how that looked. And then we need a boolean and the boolean is very simple. Is it x's turn and is it not x's turn? And that should be all we actually need for this indicator to switch. So now on the front end, we can see that it is currently crosses turn to go. If I fill that space in, we switch to noughts. And as soon as the move sinks on the right-hand side, it also switches to noughts. We now know it's noughts turn. So they can fill in a square as well and back and forward and back and forward until we've played the game. So we have a working version of real-time tic-tac-toe, right? You make a move on one computer and on a completely different computer, perhaps on the other side of the world, they see your moves in real time or close to real time. There's a couple of seconds delay.

This is just a simple explanation with no code.

دوست دارید گاهی خبرهای Laracasts را ایمیل کنیم؟