-
Notifications
You must be signed in to change notification settings - Fork 7
4. Game Process
TODO: Introduction to Websocket with dummy eco streams
72a193066497b60c871fa8a26bea67a01d1de8e8
|
---|
TODO: Exposition of the exercise 14
TODO: Introduction to compacted Topics and Consumers
d8796708155223c2066ae5640b8d8f62d1e75f2e
|
---|
TODO: Exposition of the exercise 15
TODO: Introduction to the eventHandler
TODO: Exposition of the exercise 16
TODO: Introduction to Serders and the new waiting stream
25d0cff4dc01b379c586127aafd165a9e58c1ae5
|
---|
TODO: Exposition of the exercise 17
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.
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
Cmd
s
Let's continue with the same approach of divide up the exercise into smaller pieces.
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. |
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 usedContest
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
.
- Describe these changes in the case class
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.
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 |
---|
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
Waiting a game and opening WS | Waiting a game but WS is open | WebSocket error received
- In the file
WaitingGameView.scala
implement the new views as shown above. - In the file
Main.scala
implement thedef view(model: ModelIO): Html[Msg]
to show the new views accordingly.
We want actually to open a WS connection with the Server.
- Edit the file
ScalaDaysClient.scala
to implement the functiondef 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"
TODO: Introduction to how monitoring new messages in the input topic
TODO: Exposition of the exercise 19 (StartGame in the input topic)
TODO: Introduction to the new MatchStream
XXXXXX Commit 28 here
|
---|
TODO: Exposition of the exercise 20
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.
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)
)
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 asGame
.
- If works, then we emit
Msg.GameUpdate(game)
- If not. We emit
WebSocketStatus.ConnectionError
as aMsg
.- If we receives
WebSocketEvent.Heartbeat
orWebSocketEvent.Open
we emitWebSocketStatus.Nop
as aMsg
.- Finally, we emit
WebSocketStatus.ConnectionError(WebSocketError("Unknown websocket message"))
for the rest of incoming events.
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)
.
The views for the Game are something like these:
The user's turn | The opponent's turn |
---|---|
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.