Skip to content

4. Game Process

Rafa Paradela edited this page May 28, 2023 · 27 revisions

GAME SEQUENCE

Dummy websocket server

TODO: Introduction to Websocket with dummy eco streams

Exercise 14:

⚠️ Before tackling this exercise: Check out this commit 72a193066497b60c871fa8a26bea67a01d1de8e8

TODO: Exposition of the exercise 14

Compacted topics + Consumer

TODO: Introduction to compacted Topics and Consumers

Exercise 15:

⚠️ Before tackling this exercise: Check out this commit d8796708155223c2066ae5640b8d8f62d1e75f2e

TODO: Exposition of the exercise 15

EventHandler

TODO: Introduction to the eventHandler

Exercise 16:

TODO: Exposition of the exercise 16

Serders + WaitingStream

TODO: Introduction to Serders and the new waiting stream

Exercise 17:

⚠️ Before tackling this exercise: Check out this commit 25d0cff4dc01b379c586127aafd165a9e58c1ae5

TODO: Exposition of the exercise 17

Websocket connection in client

As we did with the Login process, now it is convenient to analyze how the client expects to communicate with the server and what the parties involved in the Game process are.

notitle

Exercise 18:

⚠️ Before tackling this exercise: Check out this commit XXXXXX Commit 26 here

The server already has the WebSocket endpoint ready for use by the client app, so let's work to define:

  • The possible states necessary to open a WebSocket connection, create a new game, join an existing one, receive a WS error, etc.
  • What changes does the Model demand to represent these new states
  • What events (Msg) do we need to include in order to trigger those new states
  • How the Tyrian app will react to the new events (Msg)
  • What views will show the new state of the app
  • How can we interact with a Tyrian WebSocket via a set of Cmds

Let's continue with the same approach of divide up the exercise into smaller pieces.

Part 0: What are the new states?

First thing first. What are the new states of the app?

State What is happening?
Before joining a game The user has not clicked the "Join Game" button, and the WebSocket connection is not even started
Waiting a game and opening WS The user has already pressed the "Join Game" button, but the WS Connection client/serve not ready
Waiting a game but WS is open The user has already pressed the "Join Game" button, and the WS Connection client/serve is stablished
WebSocket error received The server has responded with an error. We don't have any game yet
Game received through the WS The server has responded with a registered game.

Part 1: The Model

If we recap, the current shape of the Model includes nickname, player, and errors, which don't seem to be enough to describe completely all the new states. We propose to add the properties:

  • contest: Contest which will include a eventual game. We used Contest as a synonymous of Game but avoiding two classes with the same name.
  • ws: Option[WebSocket] which will potentially contain WS connection once is established.

The model might evolve like:

State Nickname Player Errors Contest WS
Before joining a game Non empty Player.Registered Empty Contest.Empty None
Waiting a game and opening WS Non empty Player.Registered Empty Contest.InProgress None
Waiting a game but WS is open Non empty Player.Registered Empty Contest.InProgress Some(ws)
WebSocket error received Non empty Player.Registered Non empty Contest.Empty Some(ws)
Game received through the WS Non empty Player.Registered Empty Contest.Registered Some(ws)
  • Edit the file Model.scala to:
    • Describe these changes in the case class Model
    • Update the init Model.

Part 2: The Msg

Reminder: The event LoginSuccess(playerId: PlayerId) would bring the Model to the state "Before joining a game", right?

Let's check what other events (Msg) we need to add to trigger all the new states:

State Triggered by the Msg:
Before joining a game LoginSuccess(playerId: PlayerId) (existing)
Waiting a game and opening WS WebSocketStatus(Connecting(playerId)) (new)
Waiting a game but WS is open WebSocketStatus(Connected(ws)) (new)
WebSocket error received WebSocketStatus(ConnectionError(error)) (new)
Game received through the WS GameUpdate(game) (new)
  • Edit the file Messages.scala to add these events.

Part 3: Update

Well, now we have new events to which we must react with model evolutions, as follows:

Msg: Model Cmd
WebSocketStatus(Connecting(playerId)) contest = Contest.InProgress client.connect(playerId)
WebSocketStatus(Connected(ws)) ws = ws Cmd.None
WebSocketStatus(ConnectionError(error)) errors = List(error) Cmd.None
GameUpdate(game) contest = Contest.Registered(game) Cmd.None
  • Edit the file Update.scala to react to these events as described above.
ℹ️ INFO: When the user pushes the Join Game button, the app establishes a WS connection with the server, so the Cmd client.connect(playerId) will be implemented in the part 5

Part 4: Views

As you might expect, we now need to define a view for each possible state the app can adopt. Here are some ideas:

Before joining a game

Screenshot 2023-05-28 at 5 04 36 AM

Waiting a game and opening WS | Waiting a game but WS is open | WebSocket error received

Screenshot 2023-05-28 at 4 00 57 AM

  • In the file WaitingGameView.scala implement the new views as shown above.
  • In the file Main.scala implement the def view(model: ModelIO): Html[Msg] to show the new views accordingly.

Part 5: WebSocket

We want actually to open a WS connection with the Server.

  • Edit the file ScalaDaysClient.scala to implement the function def connect(playerId: PlayerId): Cmd[F, Msg]

Tip 1: Tyrian WebSocket

At the official Tyrian docs, there is a very similar example where WebSocket.connect is used.

Tip 2: The URI of the WebSocket endpoint

As we implemented in the server side, it would be val uri = wsUri / "player" / playerId.show / "join"

Magic monitoring: StartGame in the input topic

TODO: Introduction to how monitoring new messages in the input topic

Exercise 19:

TODO: Exposition of the exercise 19 (StartGame in the input topic)

GameStream: matchStream

TODO: Introduction to the new MatchStream

Exercise 20:

⚠️ Before tackling this exercise: Check out this commit XXXXXX Commit 28 here

TODO: Exposition of the exercise 20

Receive Game in client

In the client, we have provided the app with the ability to create WS connections, and, consequently, to create new games. However, given the two-way nature of these types of connections, we are not yet processing the events of new games created on the server. And that is what we are going to address in this section.

At this point, we must introduce a nice ability that Tyrian implements to listen to events of different kinds. And Tyrian's documentation defines it as:

Subscriptions (Subs) are used to observe something that changes over time, and to emit discrete messages when something happens.

The Tyrian.WebSocket lets us generate new subscriptions (Sub[F, Msg]) by passing a transformation from WebSocketEvent to Msg. Something like def subscribe[Msg](f: WebSocketEvent => Msg): Sub[F, Msg], which is extremely convenient because we have a potential WebSocket in our Model.

Exercise 21:

⚠️ Before tackling this exercise: Check out this commit XXXXXX Commit 30 here

In the Main.scala we already are checking if our Model contains a WebSocket in order to observe its events:

def subscriptions(model: ModelIO): Sub[IO, Msg] =
  model.ws.fold(Sub.emit(Msg.WebSocketStatus(WebSocketMessage.WebSocketStatus.Disconnected)))(ws =>
    scalaDaysClient.handleWebSocket(ws)
  )

Part 1: Handle the WebSocket

But now it's time to implement scalaDaysClient.handleWebSocket(ws) (from ScalaDaysClient.scala) having these tips into account:

  • Remember the signature def subscribe[Msg](f: WebSocketEvent => Msg): Sub[F, Msg]
  • We are going to react to WebSocketEvent.Receive(message) by decoding the message as Game.
    • If works, then we emit Msg.GameUpdate(game)
    • If not. We emit WebSocketStatus.ConnectionError as a Msg.
  • If we receives WebSocketEvent.Heartbeat or WebSocketEvent.Open we emit WebSocketStatus.Nop as a Msg.
  • Finally, we emit WebSocketStatus.ConnectionError(WebSocketError("Unknown websocket message")) for the rest of incoming events.

Part 2: React to Msg.GameUpdate(game)

In the Update.scala we want to react to the new event Msg.GameUpdate(game). Please make sure the Model evolves with contest = Contest.Registered(game) when we receive a Msg.GameUpdate(game).

Part 3: If we receive Game, now we have to show it

The views for the Game are something like these:

The user's turn The opponent's turn
notitle Screenshot 2023-05-28 at 4 01 20 AM

The Hmlt[Msg] for these views are not trivial, so we have partially implemented them at GameView.scala. But there is still one missing part. The method def CellView(position: Position): Html[Msg] needs implementation, and renders a button for the given position.

Tip 1: Enabled or Disabled

The button should be disabled if it not your turn, or if there is a movement at this position.

Tip 2: OnClick when the button is enabled

When the button is enabled, the onClick action should send a Msg.RequestNewMovement(game, newMovement) (which is a new Msg what we'll use in the next section.

Clone this wiki locally