An example room implemented in Lagom for Game On! (https://gameontext.org)
Lagom is a framework for developing reactive microservices in Java or Scala. Created by Lightbend, Lagom is built on the proven Akka toolkit and Play Framework, and provides a highly productive, guided path for creating responsive, resilient, elastic, message-driven applications.
Game On! is both a sample microservices application, and a throwback text adventure brought to you by the WASdev team at IBM.
This project was created by the Maven archetype from https://github.com/lagom/lagom-gameon-maven-archetype.
See the README.md
file in that repository for more information.
Log in to Bluemix with the workshop email address you were provided:
bx login -a https://api.ng.bluemix.net -c 1e892c355e0ba37560f028df670c2719
You will be prompted for the email address and password. Enter these as provided to you.
Initialize the Bluemix Container Service plugin:
bx cs init
Download the configuration files for the Kubernetes cluster:
bx cs cluster-config gameon
This should print details similar to the following, though the details might differ slightly:
OK
The configuration for javaone was downloaded successfully. Export environment variables to start using Kubernetes.
export KUBECONFIG=/home/workshop/.bluemix/plugins/container-service/clusters/gameon/kube-config-dal10-gameon.yml
Copy and run the provided export
command to configure the Kubernetes CLI. Note that you should copy the command printed to your terminal, which might differ slightly from the example above.
Test that you can run kubectl
to list the resources in the Kubernetes cluster:
kubectl get all
Log in to the Bluemix Container Registry:
bx cr login
Build the Docker image for your service:
mvn clean package docker:build
Push the Docker image to the Bluemix registry:
docker tag javaone/gameon17s99-impl:1.0-SNAPSHOT registry.ng.bluemix.net/javaone/gameon17s99-impl:1.0-SNAPSHOT
docker push registry.ng.bluemix.net/javaone/gameon17s99-impl:1.0-SNAPSHOT
Deploy the service to Kubernetes:
kubectl create -f deploy/kubernetes/resources/service/
Wait for the service to begin running:
kubectl get -w pod gameon17s99-0
Press control-C to exit once this prints a line with “1/1” and “Running”.
Your room is up!
Do a quick verification: http://169.60.34.197/gameon17s99
Go to Game On! and sign in.
Click on the building icon in the top right of the game screen to go to the Room Management page.
Make sure Create a new room is selected from the Select a room drop-down.
Provide a descriptive title for your room, e.g. Paul's Diner
, ‘The Red Caboose’, …
A short nickname will be generated, but please change the value to gameon17s99
.
Describe your room (optional). The description provided here is used by the interactive map and other lists or indexes of defined rooms. The decription seen in the game will come from your code.
The repository field is optional. Come back and fill it in if you decide to push this into a public repository.
Specify the http endpoint as a basic health endpoint: http://169.60.34.197/gameon17s99
Use a WebSocket URL for the WebSocket endpoint: ws://169.60.34.197/gameon17s99
Leave the token blank for now. That is an Advanced adventure for another time.
Describe the doors to your room (Optional). Describe each door as seen from the outside
Click Register to register the room and add it to the Map!
You can come back to this page to update your room registrations at any time. Choose the room you want to update from the drop-down, make any desired changes, and click either Update to save the changes or Delete to delete the registration entirely.
Use the arrow in the top right to go back to the game screen. Go Play!
/help
to see available commands (will vary by room)./exits
to list the exits in the room.Remember that shortname you set earlier? To visit your room:
/teleport <nickname>
It should show something like this:
Connecting to Game On Lab. Please hold.
Room’s descriptive full name
Lots of text about what the room looks like
That isn’t very original now, is it? For a first kick of the tires, let’s make that a little more friendly.
Import your project into IDE of choice
Using Eclipse
maven
to filter and select Existing Maven project.~/gameon17s99
.Using IntelliJ IDEA
~/gameon17s99
.Importing the ~/gameon17s99
folder into an IDE will create two folders of note:
gameon17s99-api
— service apigameon17s99-impl
— service implementationIn the gameon17s99-impl
project, look in src/main/java
to open com/lightbend/lagom/gameon/gameon17s99/impl/Room.java
The following constants are defined near line 26:
static final String FULL_NAME = “Room’s descriptive full name”;
static final String DESCRIPTION = “Lots of text about what the room looks like”;
Change those to something that suits you better!
Rebuild the docker image
mvn clean package docker:build
Re-tag and push the updated Docker image to the Bluemix registry:
docker tag javaone/gameon17s99-impl:1.0-SNAPSHOT registry.ng.bluemix.net/javaone/gameon17s99-impl:1.0-SNAPSHOT
docker push registry.ng.bluemix.net/javaone/gameon17s99-impl:1.0-SNAPSHOT
To make our updated Room service live, we just need to delete the pod and let Kubernetes recreate it. The ‘always’ image pull policy ensures that Kubernetes will grab the latest Docker image when it recreates the pod.
kubectl delete pod gameon17s99-0
Wait for the service to begin running:
kubectl get -w pod gameon17s99-0
Press control-C to exit once this prints a line with “1/1” and “Running”.
If you go back to the game now, you should see your changes (as the game will reconnect the websocket when your service comes back).
Let’s now walk through making a simple custom command: /ping
Open your Room implementation in your editor again (if you happened to close your IDE in the meanwhile, remember it is com/lightbend/lagom/gameon/gameon17s99/impl/Room.java
under src/main/java
in the gameon17s99-impl
project).
Around line 37 is a /ping
command. You’ll need to uncomment that line, and remove the semi-colon ahead of it to add the /ping
command to the list of commands known to your room. It should look something like this (clean it up more if you’d like):
static final PMap<String, String> COMMANDS = HashTreePMap.<String, String>empty()
// Add custom commands below:
.plus("/ping", "Does this work?");
// Each custom command will also need to be added to the `handleCommand` method.
That comment above helpfully tells us what to edit next. Let’s find the handleCommand
method. It is lurking somewhere around line 79. The parseCommand
method has removed the leading slash from the command, so we only have to look for “ping”. Add something like this to the switch statement:
case "ping":
handlePingCommand(message, command.get().argument);
break;
Now we have to define the new method. To take best advantage of cut and paste and place it near things that are alike, we’ll put it by handleUnknownCommand
, near line 166. In fact, let’s just cut and paste the handleUnknownCommand method, and change the name and arguments:
private void handlePingCommand(RoomCommand pingCommand, String argument) {
Event pingCommandResponse = Event.builder()
.playerId(pingCommand.getUserId())
.content(HashTreePMap.singleton(
pingCommand.getUserId(), UNKNOWN_COMMAND + argument
))
.bookmark(Optional.empty())
.build();
reply(pingCommandResponse);
}
This method takes in a command and packages a response, which it then sends.
There are some changes we need to make to this command. An obvious one is replacing that UNKNOWN_COMMAND
constant. But before we take off to do that, we should take a closer look at that response. As currently defined, the ping response is specific: it will only go back to the player that initiated it. Let’s tell everyone that the player is playing pingpong. The WebSocket protocol for events specifies how to deliver content to everyone, and further, how to direct some content to one player, and other content to everyone else. Focusing on the pingCommandResponse
formation. We need to make the following changes:
*
Add two entries to the content map, one for the player, and one for everyone else.
All told, it should look something like this:
Event pingCommandResponse = Event.builder()
.playerId(ALL_PLAYERS)
.content(HashTreePMap.<String, String>empty()
.plus(ALL_PLAYERS, pingCommand.getUserId() + PINGPONG)
.plus(pingCommand.getUserId(), PONG + argument))
.bookmark(Optional.empty())
.build();
Now lets go to the top to define those constants (near line 50).
private static final String ALL_PLAYERS = "*";
private static final String PINGPONG = " is playing pingpong";
private static final String PONG = "pong: ";
There should be no compilation errors (as reported by your IDE) at this point. Let’s try adding a test to make sure this works. Open com/lightbend/lagom/gameon/gameon17s99/impl/RoomServiceIntegrationTest.java
under src/test/java
in the gameon17s99-impl
project). We’ll add our new test as a neighbor to the test for the Unknown command again, which is somewhere around line 244. Add a test method that looks something like the following. Note that we’ve typed more explicitly what we expect to be in the message.
@Test
public void broadcastsPingCommands() throws Exception {
try (GameOnTester tester = new GameOnTester()) {
tester.expectAck();
RoomCommand pingMessage = RoomCommand.builder()
.roomId("<roomId>")
.username("chatUser")
.userId("<userId>")
.content("/ping Hello, world")
.build();
tester.send(pingMessage);
Event unknownCommandEvent = Event.builder()
.playerId("*")
.content(HashTreePMap.<String, String>empty()
.plus("*", "<userId> is playing pingpong")
.plus("<userId>", "pong: Hello, world"))
.bookmark(Optional.empty())
.build();
tester.expect(unknownCommandEvent);
}
}
There should be no compilation errors at this point. We can revisit the previous steps to work with our new room
mvn clean package docker:build
docker tag javaone/gameon17s99-impl:1.0-SNAPSHOT registry.ng.bluemix.net/javaone/gameon17s99-impl:1.0-SNAPSHOT
docker push registry.ng.bluemix.net/javaone/gameon17s99-impl:1.0-SNAPSHOT
kubectl delete pod gameon17s99 && kubectl get -w pod gameon17s99-0
Add bazaar api
as a dependency to your impl
project.
In the GameOnRoomModule
implement the client interface
bindClient(Bazaar.Service.class);
This will make an instance of the Bazaar service injectable via Guice.
In the RoomServiceImpl
constructor add parameter BazaarService bazaarService
. This makes the service available to the implementation.
In the RoomServiceImpl
actor creation, add the bazaarService
as a parameter.
In Room.java
add a private final
for bazaarService
of type BazaarService
.
In Room.java
update the actor props with bazaarService
as a parameter.
Add bazaarService
to the Room
objects constructor.
Set the local bazaarService
value upon Room
construction.
In Room.java
update the PMap
to included GameOn
commands for getting and putting an item to the bazaar.
.plus(“/getBazaar”, “Descriptive text goes here”)
.plus(“/putBazaar”, “Descriptive text goes here”);
In Room.java
, in the handleCommand
method add cases to handle new commands.
case: “getBazaar”:
handleGetBazaar(message);
break;
case: “putBazaar”:
handlePutBazaar(message, command.get().arguments);
break;
In Room.java
implement methods handleGetBazaar
and handlePutBazaar
similar to the way you implemented the ping
command.
private void handleGetBazaar(RoomCommand getBazaarCommand) {
ActorRef sender = sender(); // capture sender() to use in a CompletionStage
bazaarService.bazaar().invoke().thenAccept(item -> {
Event getBazaarCommandResponse = Event.builder()
.playerId(getBazaarCommand.getUserId())
.content(HashTreePMap.singleton(
getBazaarCommand.getUserId(), "Bazaar contained: " + item
))
.bookmark(Optional.empty())
.build();
reply(getBazaarCommandResponse, sender);
});
}
private void handlePutBazaar(RoomCommand putBazaarCommand, String item) {
ActorRef sender = sender(); // capture sender() to use in a CompletionStage
bazaarService.useItem().invoke(new ItemMessage(item)).thenAccept(done -> {
Event putBazaarCommandResponse = Event.builder()
.playerId(putBazaarCommand.getUserId())
.content(HashTreePMap.singleton(
putBazaarCommand.getUserId(), "Put " + item + " into the Bazaar"
))
.bookmark(Optional.empty())
.build();
reply(putBazaarCommandResponse, sender);
});
}