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!
- Use
/help
to see available commands (will vary by room). - Use
/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
- Start Eclipse
- File -> Import,
- Type
maven
to filter and select Existing Maven project. - Click Next
- Navigate to your project folder
~/gameon17s99
. - Click Finish
-
Using IntelliJ IDEA
- From the Welcome screen, click Import project
- Navigate to your project folder
~/gameon17s99
. - Click OK
- On the 'Import Project' screen, select Import from external model and choose
- Allow maven projects to Import projects automatically. Use default values and the Next button until you get to click Finish
-
Importing the
~/gameon17s99
folder into an IDE will create two folders of note:gameon17s99-api
-- service apigameon17s99-impl
-- service implementation
-
In the
gameon17s99-impl
project, look insrc/main/java
to opencom/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
undersrc/main/java
in thegameon17s99-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. TheparseCommand
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 thepingCommandResponse
formation. We need to make the following changes:- Target all players using
*
- 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();
- Target all players using
-
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
undersrc/test/java
in thegameon17s99-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 yourimpl
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 parameterBazaarService bazaarService
. This makes the service available to the implementation. -
In the
RoomServiceImpl
actor creation, add thebazaarService
as a parameter. -
In
Room.java
add aprivate final
forbazaarService
of typeBazaarService
. -
In
Room.java
update the actor props withbazaarService
as a parameter. -
Add
bazaarService
to theRoom
objects constructor. -
Set the local
bazaarService
value uponRoom
construction.
- In
Room.java
update thePMap
to includedGameOn
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 thehandleCommand
method add cases to handle new commands.
case: “getBazaar”:
handleGetBazaar(message);
break;
case: “putBazaar”:
handlePutBazaar(message, command.get().arguments);
break;
- In
Room.java
implement methodshandleGetBazaar
andhandlePutBazaar
similar to the way you implemented theping
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);
});
}