"{Playername} wins"
to finish the game and record the victor in a store.The blind is now *y*
without having to refresh the browser. We can use WebSockets to facilitate this.WebSocket is a computer communications protocol, providing full-duplex communication channels over a single TCP connection
/game
.GET /game
that we get a 200
.game
methodnewGameRequest
to make the request to /game
. Try writing this yourself.assertStatus
to accept response
rather than response.Code
as I feel it reads better.WebSocket
is built into most modern browsers so we don't need to worry about bringing in any libraries. The web page won't work for older browsers, but we're ok with that for this scenario.game.html
html/template
is a Go package for creating HTML. In our case we call template.ParseFiles
, giving the path of our html file. Assuming there is no error you can then Execute
the template, which writes it to an io.Writer
. In our case we want it to Write
to the internet, so we give it our http.ResponseWriter
.cmd/webserver
and run the main.go
file. Visit http://localhost:5000/game
.game.html
in the cmd/webserver
directory. I chose to create a symlink (ln -s ../../game.html game.html
) to the file inside the root of the project so if I make changes they are reflected when running the server.go get github.com/gorilla/websocket
websocket
library. My IDE automatically did it for me, so should yours.httptest.NewServer
which takes a http.Handler
and will spin it up and listen for connections.websocket.DefaultDialer.Dial
we try to dial in to our server and then we'll try and send a message with our winner
./ws
so we're not shaking hands yet.webSocket
handlerUpgrade
the request. If you now re-run the test you should move on to the next error.conn.ReadMessage()
blocks on waiting for a message on the connection. Once we get one we use it to RecordWin
. This would finally close the WebSocket connection.time.Sleep
before the final assertion.upgrader
to a private value inside our package because we don't need to redeclare it on every WebSocket connection requesttemplate.ParseFiles("game.html")
will run on every GET /game
which means we'll go to the file system on every request even though we have no need to re-parse the template. Let's refactor our code so that we parse the template once in NewPlayerServer
instead. We'll have to make it so this function can now return an error in case we have problems fetching the template from disk or parsing it.PlayerServer
NewPlayerServer
we now have compilation problems. Try and fix them yourself or refer to the source code if you struggle.mustMakePlayerServer(t *testing.T, store PlayerStore) *PlayerServer
so that I could hide the error noise away from the tests.mustDialWS
so that I could hide nasty error noise when creating the WebSocket connection./game
. You should see them recorded in /league
. Remember that every time we get a winner we close the connection, you will need to refresh the page to open the connection again.game.html
to update our client side code for the new requirementsconn.onmessage
we assume to be blind alerts and so we set the blindContainer.innerText
accordingly.Game
so our CLI code could call a Game
and everything else would be taken care of including scheduling blind alerts. This turned out to be a good separation of concern.Start
the game which would kick off the blind alerts and when the user declared the winner they would Finish
. This is the same requirements we have now, just a different way of getting the inputs; so we should look to re-use this concept if we can.Game
is TexasHoldem
BlindAlerter
TexasHoldem
can schedule blind alerts to be sent to whereverBlindAlerter
we use in the CLI.os.Stdout
but this won't work for our web server. For every request we get a new http.ResponseWriter
which we then upgrade to *websocket.Conn
. So we can't know when constructing our dependencies where our alerts need to go.BlindAlerter.ScheduleAlertAt
so that it takes a destination for the alerts so that we can re-use it in our webserver.to io.Writer
StdoutAlerter
doesn't fit our new model so just rename it to Alerter
TexasHoldem
because it is calling ScheduleAlertAt
without a destination, to get things compiling again for now hard-code it to os.Stdout
.SpyBlindAlerter
no longer implements BlindAlerter
, fix this by updating the signature of ScheduleAlertAt
, run the tests and we should still be green.TexasHoldem
to know where to send blind alerts. Let's now update Game
so that when you start a game you declare where the alerts should go.TexasHoldem
so it properly implements Game
CLI
when we start the game, pass in our out
property (cli.game.Start(numberOfPlayers, cli.out)
)TexasHoldem
's test i use game.Start(5, ioutil.Discard)
to fix the compilation problem and configure the alert output to be discardedGame
within Server
.CLI
and Server
are the same! It's just the delivery mechanism is different.CLI
test for inspiration.GameSpy
Game
and pass it into mustMakePlayerServer
(be sure to update the helper to support this).mustMakePlayerServer
in other tests. Introduce an unexported variable dummyGame
and use it through all the tests that aren't compilingGame
to NewPlayerServer
but it doesn't support it yetGame
as a field to PlayerServer
so that it can use it when it gets requests.game
so rename that to playGame
)Game
within webSocket
.game.Start
we send in ioutil.Discard
which will just discard any messages written to it.main.go
to pass a Game
to the PlayerServer
Game
with PlayerServer
and it has taken care of all the details. Once we figure out how to send our blind alerts through to the web sockets rather than discarding them it should all work.io.Writer
for the game to write the blind alerts to.playerServerWS
from before? It's our wrapper around our WebSocket so it feels like we should be able to send that to our Game
to send messages to.playerServerWS
does implement io.Writer
. To do so we use the underlying *websocket.Conn
to use WriteMessage
to send the message down the websocketTexasHoldem
so that the blind increment time is shorter so you can see it in actionStartGame
was playerServerWS
rather than ioutil.Discard
so that might make you think we should perhaps spy on the call to verify it works.GameSpy
does not send any data to out
when you call Start
. We should change it so we can configure it to send a canned message and then we can check that message gets sent to the websocket. This should give us confidence that we have configured things correctly whilst still exercising the real behaviour we want.BlindAlert
field.GameSpy
Start
to send the canned message to out
.PlayerServer
when it tries to Start
the game it should end up sending messages through the websocket if things are working right.wantedBlindAlert
and configured our GameSpy
to send it to out
if Start
is called.ws.ReadMessage()
to wait for a message to be sent and then check it's the one we expected.ws.ReadMessage()
will block until it gets a message, which it never will.