Overview

In this lab, you will be implementing a scoreboard that updates in real-time with the scores of other players. As you are playing the game and accumulating points, your Boggle client will simultaneously broadcast your score to other players (which in our case, will just be other tabs). While the majority of the computation will be performed on the server, the real-time functionality will be achieved through the use of WebSockets.

Why WebSockets?

An astute reader may have noticed that this same functionality can be accomplished via Ajax: One could just send a POST request to the server with the current player's score periodically, and perform GET requests to retrieve information about other players. So, why use WebSockets? When communicating via Ajax, a new connection is established each time a message is sent. With WebSockets, a single, bidirectional connection is kept open, which eliminates the performance overhead of opening and closing connections. Another key advantage to using WebSockets is that communication between a client and server does not need to conform to a request-response model in which the client must issue a request to receive a response. Instead, the server can use the WebSocket connection to efficiently push updates to connected clients without a request. In this lab, we will utilize this capability of WebSockets to allow our Boggle server to push scores to players.

Setup

The github assignment for this lab can be found here.

Note: You will have to undo your board size change from the previous lab in order for your GUI to run properly again. Go ahead and change the size back to 4 in your Board.java.

Protocol

To better structure the messages that are sent back and forth between clients and the server, we have implemented a MESSAGE_TYPE enum that lets a receiver know what data to extract from a message.

Communication should happen through JSON, which you first encountered in the Javascript lab. Every message between server and client should be a JSON object that includes two keys: type and payload. Type is an integer representing the type of the message, and payload is a JSON object that varies as described below:

MESSAGE_TYPE Payload Schema
CONNECT

id: the unique identifier that the server has assigned to the client

SCORE

id: the unique identifier of the client

board: the client’s board

text: the contents of the player's guess text field

UPDATE

id: the unique identifier of a client

score: the score of the client

You can call <ENUM>.oridinal() to access the integer representation of an enum.

Technical Overview

  1. When the client first loads, a Websocket connection is established between the client and server.
  2. The server sends a message of type CONNECT to the client that includes a unique identifier that the client will use to identify itself from then on.
  3. The client will send a message of type SCORE to the server whenever the player's score changes that will include the unique identifier of the client, the board, and the player's guesses.
  4. When the server receives a message of type SCORE from a client, it will broadcast an UPDATE message to each of its connected clients with the unique identifier of the player whose score was changed and the score itself.
  5. When the client receives an UPDATE message, it will update its scoreboard with the unique identifier and corresponding score from the message.

Server Functionality

First, we will add the necessary WebSocket functionality to the server. Spark expects two fields when instantiating a WebSocket: an endpoint that can be anything you want and a class with annotated methods that act as event handlers.

To start off, in your runSparkServer method, add the following line before any other HTTP routes are defined:

Spark.webSocket("/scores", ScoringWebSocket.class);

This tells Spark that we want to run a WebSocket with functionality defined by the class ScoringWebSocket, and we want it to listen at the path /scores for connections. This class contains three methods. Implement each of the methods as described below:

connected(Session)

First, add the new session to the sessions queue. Next, build and send a CONNECT message to the new session. Take a look at the doc for JsonObject and build your message as a JsonObject. Refer to the Protocol section above. Lastly, you can send a message to a session using the method <session>.getRemote().sendString(String). JsonObjects can be converted into strings by calling GSON.toJson(<JsonObject>). Don't forget to increment nextId at the end!

closed(Session, int, String)

Remove the session from the queue.

message(Session, String)

Compute the player's score by finding the sum of all the scores for the player's words. The received object is a protocol of type SCORE and contains the information for the current board as well as the user's guesses. You can access this information in the payload, as specified in the Protocol section above. As an example of accessing different attributes of the JsonObject, you can access the "payload" attribute of the received JsonObject by calling received.get("payload").getAsJsonObject(). Again, take a look at the doc for JsonObject!

Ensure that only valid words are scored. You can do this by checking the player's words against the set of all valid words on a board. Then, build and send an UPDATE message to all connected users notifying them of the player's new score.

Client Functionality

After the server is functional, we will add the necessary functionality to the client. First, add the following snippet to the bottom of your play.ftl file:

<script>
$(document).ready(function() {
  setup_live_scores();
});
</script>

Next, you must create the WebSocket connection in the setup_live_scores function in websockets.js. A new WebSocket takes in a single argument: the address of the socket. For our purposes, you can create a new WebSocket using:

new WebSocket("ws://localhost:4567/scores")

Assign this WebSocket to the conn variable. Next, you will be completing two other functions in websockets.js:

conn.onmessage

This function is called whenever the WebSocket receives a message from the server. There are two possible types of messages: CONNECT and UPDATE. For CONNECT, simply set the myId variable to your proper id, as determined by the server. The UPDATE message is a bit more involved. You will want to display the new score that you just received in your GUI. You should do so by adding an element on the page that shows the user ID and their new score (you can add this in your form.ftl). If you'd like, you can expand your project to use usernames rather than the programmatically generated user IDs (but this is not required for check off – we just want to see that you have web sockets working!).

new_guess

This defines a method that should be called whenever a user guesses a new word. Be sure to add a call to this function in the proper place in boggle.js.

Inside the new_guess function, you should send a SCORE message to the server informing it of the user's guesses. Simply create a JSON object with the proper attributes and send over the stringified version by calling conn.send(<string>).

Finally, don't forget to include websockets.js in your main.ftl file using a script tag. Refer back to the HTML lab if you forgot how to do this.

Does it work?

If you have implemented everything correctly, you will be able to start up games of Boggle on separate tabs. First, navigate to your /list page that you used during the database lab, and open up another board in a different tab. As you play on one tab, you should see your score reflected in the other tab!

Getting checked off

Congrats, you've finished the lab! Push your project to GitHub and then show your functioning multiplayer Boggle to a TA in order to get checked off.