Implementing a Multi-Player XMPP Game – Part 1

In most of my previous blogs, I have focused on what are the new features in the Vorpal framework. Vorpal has now reach a stage of development where I feel that I can stop adding new features and let the existing one stabilizes. One aspect that really concerns me is how useful are those features that I’ve added?

So for the next few blogs I’ll focus on using Vorpal to develop a moderately complex (okay hello_world++) XMPP application; the aim is to show how to use Vorpal to develop something useful (as oppose to code snippets) and also the larger issue of using XMPP in your application.

I’m no XMPP expert and mostly what I know comes from reading the standards, books (one book in particular) and this excellent series of blogs on the design of Chesspark (part 1, 2 and 3).

Set Game

The Set game is a real-time, party style card game. 12 cards are laid out in 4 rows of 3 on the table. Each card has the following four attributes: colour, number, symbol and shading; each attribute has 3 different values. The example card shown on the right has the following attributes: colour: red, number: 2, symbol: diamond, shading: open.

The game is very simple; you are to pick 3 cards that have the same 3 attributes; this constitute a set. Alternatively you can pick 3 cards, all with different attributes. You can see some examples here. The cards are replenished from the deck after you have removed your set. Game ends when the deck runs out of card and there are no more set on the table. The winner is the player with the most set.

Detailed rules and examples can be found here.

Design

We will implement the Set game using MUC (multi-user chat); the reason is that since this is a real-time, multi-player party style game, we think that MUC would be a good fit. The other option is to use pubsub. Every MUC set game room has a ‘dealer’. The dealer is the entity that is responsible for keeping score, checking if you have a set, replenishing cards, etc. The dealer is our Set game component.

Players join the game by entering the game room. Once they’re in the game room, they

  • will be notified of who else is in the game room. Also when player leaves, you will also be notified. All these are supported through MUC.
  • find the game room by sending a disco#items to the set game service
  • get the state of the board from the from the dealer
  • try to take a set of 3 cards by informing the dealer. This is between the dealer and the player only (‘chat’ message)
  • dealer will inform everyone if a player is successful in taking a set (‘groupchat’ message)

Game Protocol

The game protocol can be roughly broken up into 3 parts

  1. A player entering and leaving the game room – these are standard MUC messages
  2. Game specific messages – these are game messages. We will use <message> for all Set game moves. Game messages from the dealer are also <message> type.
  3. Game room membership – messages sent out by MUC about players joining and leaving the game room

Implementation

Setting up the Game Room

When the game starts up, the first thing that it’ll do is to setup the game room. In this implementation, we only support one game room; you are welcome to take the code and modify to support more. We do this by listening to the ComponentPostStart event which is after the Set game component has successfully connect to the XMPP server; see here for more details on lifecycle events.

private void postStart(@Observes ComponentPostStart postStartEvt) {
   String confRoom = randomString(16) + "@conference."
+ postStartEvt.getDomain();
   gameState.setRoomName(confRoom);
   JoinRoom join = new JoinRoom(Constants.DEALER + "@"
+ postStartEvt.getSubdomain(), confRoom + "/" + Constants.DEALER);
    try {
        postStartEvt.getComponentContext().send(join);
    } catch (ComponentException ex) { }
}

The game room’s name, called confRoom above, is randomly generated. The Set game component then joins the room with the nick name dealer. The following message will be sent out.

<presence from="dealer@set.batcomputer"
      to=somerandomstringname@conference.batcomputer/dealer>
   <x xmlns="
http://jabber.org/protocol/muc"/>
</presence>

We perform the reverse when the component shuts down by destroying the room.

gameState is an @ApplicationScoped CDI managed object. It stores information like the game room, card deck, scores, etc. As you can see above, once we have generated the game room, we save that in the gameState object.

Once we have send the join room message, we now have to wait for a confirmation from the XMPP server. The message will come in the form of a <presence> message.

@Presence
@From("{room}@conference." + PredefinedBindings.PARAMETER_DOMAIN + "/{ignore}")
public class ConfirmChatRoom {

   @Inject GameState gameState;
   @Inject ComponentContext compCtx;

   @XmlPath(namespace={"n", Constants.MUC_USER}, path="/presence/n:x/n:item[@role=’moderator’]")
   private ResponseContext roomCreated() {

      ResponseContext respCtx = new ResponseContext(ResponseContext.Type.IQ);
      respCtx.add(IQ.Type.set)
            .add(new IQChildElement("query", Constants.MUC_OWNER))
            .add(DataFormBuilder.create(DataForm.Type.submit));

      compCtx.add(new ItemSpecification(gameState.getRoomName(), "Set game room name"));
      compCtx.add(new FeatureSpecification(PredefinedBindings.DISCO_ITEMS));

      return (respCtx);
    }
}

We look for the confirmation message be examining the <presence> message with an <item> tag in the MUC namespace; furthermore the <item> must have the value moderator in the role attribute. This is our cue; the role moderator tell us that we are the room owner. So when we received this message, we can now enter the room.

To enter the room, we create an empty data form of Submit type and send that back to the room in an IQ-set message. This is shown in green.

The other thing that we need to do is to make our game room discoverable by disco#info and disco#item. Since the game room’s name is not going to change throughout the life time of the Set game component, we use static registration (in blue). We add disco#item feature to our Set game component JID; then we add the game room’s JID as in <item>.

Note on using @XmlPath annotation for matching; I’ve often @XmlElement. @XmlElement is the simpler of the 2. If you want to either match immediate child under the packet’s root (<message>, <presence> or <iq>) use @XmlElement. Use @XmlPath, based on XPath, gives you more control and matching capability at the expense of simplicity.

Handling Player Membership

As a result of using MUC, our component don’t really have to do that much when players join or leave the game room. Since the Set component is in the game room, it will receive <presence> message for every player that joins or leave the room. Furthermore, the MUC will also send notification of the new player to every other participant in the game room. Nice!

The following is an example message of a player joining the game room

<presence from=”fred@batcomputer/pidgin”
      to=”somerandomstringname@conference.batcomputer/fred”>
   <x xmlns=”http://jabber.org/protocol/muc#user”>
      <item affiliation=”member” role=”participant”/>
   </x>
</presence>

The following presence message handler handles both players joining and leaving the game room.

@Presence
@From("{roomName}@conference." + PredefinedBindings.PARAMETER_DOMAIN + "/{alias}")

public class PlayerMembershipHandler {

   @Inject GameState gameState;

   @XmlPath(namespace={"n", Constants.MUC_USER}, path="/presence/n:x/n:item[@role=’participant’]")
   private void playerJoining(@Named("alias") String player) {
      gameState.createPlayer(player);
   }

   @XmlPath(namespace={"n", Constants.MUC_USER}, path="/presence/n:x/n:item[@role=’none’]")
   private void playerLeaving(@Named("alias") String player) {
      gameState.removePlayer(player);
   }

}

The way that these membership messages are matched is exactly the same as confirming the chat room. We can actually combine the PlayerMembershipHandler and ConfirmChatRoom by assigning the @XmlPath element to a capture parameter of the type Element. I’ll leave this as an exercise to the reader.

Playing Set

The Set implementation have 2 types of game message both using <message> as envelop. The first of these 2 message is the get_board message. This message is for a new participant to get the current state of the board. The message is send directly to the dealer.

<message from="fred@batcomputer/pidgin"
      to=”somerandomstringname@conference.batcomputer/dealer”  type="chat">
   <set xmlns="uri:game:set" command="get_board"/>
</message>

The handler for get_board message is as follows

@Message @XmlElement(tag=Constants.SET_TAG)
@To(Constants.DEALER + "@" + PredefinedBindings.PARAMETER_SUBDOMAIN)
public class BoardLayoutCommandHandler {
   @Inject GameState gameState;
   @XmlPath(namespace={"n", Constants.SET_NAMESPACE}, path="/message/n:set[@command=’get_board’]")
   @From("{from}")
   public List<Object> handle(@Named(“from”) String from) {
      //Construct and return the board – see source
   }

which generates the following message

<message type="chat" to=somerandomstringname@conference.batcomputer/fred 
     from=”somerandomstringname@conference.batcomputer/dealer“>
   <ns2:set xmlns:ns2="uri:game:set" command="get_board">
      <ns2:data>
         <board_layout>
            <board id="0">22</board>
            <board id="1">33</board>
            <board id="2">78</board>
            <board id="3">43</board>
              …
            <board id="9">16</board>
            <board id="10">59</board>
            <board id="11">69</board>
         </board_layout>
      </ns2:data>
   </ns2:set>
</message>

The second message is a try_take message

<message to="somerandomstringname@setgame.batcomputer/dealer" type="chat"
      from=”
fred@batcomputer/pidgin”>
   <set xmlns="uri:game:set" command="try_take">
      <data>
         <cards_taken>
            <board id="9"/>
            <board id="7"/>
            <board id="10"/>
         </cards_taken>
      </data>
   </set>
</message>

As the name implies, a try_take is where a player tries to take 3 card to form a set. The cards are identified by their board position. Like the get_board message, the try_take command is sent directly to the dealer (message type is chat) and not broadcast to the game room.

The message handler for try_take is very similar to get_board, so I’m not going to list it here. Look at the source if you’re interested.

Finally if the try_take is successful, dealer will send a take_card message into the game room (message type is groupchat) notifying everyone of the new cards and the score

<message type="groupchat" to="somerandomstringname@conference.batcomputer"
      from=”somerandomstringname@conference.batcomputer/dealer”>
   <ns2:set xmlns:ns2="uri:game:set" command="take_card">
      <ns2:data>
         <cardsInDeck>66</cardsInDeck>
         <cards_taken>
            <board id="9">42</board>
            <board id="7">16</board>
            <board id="10">20</board>
         </cards_taken>
         <cards_new>
           <board id="9">08</board>
           <board id="7">60</board>
           <board id="10">44</board>
         </cards_new>

         <name>fred</name>
         <score>1</score>
         <taken>true</taken>
      </ns2:data>
   </ns2:set>
</message>

If try_take is not successful, the take_card message will not have the <cards_new> section and the value in <taken> element will be false.

I’ve implemented most of the game except for the end game; at the moment the component does not known when the game ends and declares a winner. I’ll try fixing this in the future.

You can find the source code of this in playground. Let me know what you think.

My next blog talk about writing a client for this Set game component.

%d bloggers like this: