Tutorials/simpletournament

From CubeiaWiki

Jump to: navigation, search

Contents

Tic-tac-toe Tournament Tutorial

Introduction

In this tutorial, we will create a very basic tic-tac-toe tournament. It will be a sit&go1 heads-up2 tournament for four players.

In a Rush?

As always, if you are in a rush, just get the finished project and start playing around with it. You need the game module, the tournament module and the client. Then:

  1. Unzip the tournament module, run mvn install.
  2. Unzip the game module, run mvn package firebase:run.
  3. Then unzip the client, open index.html in four tabs.
  4. Log in (with playerId as password) on each tab and register to the tournament, which will then start.

The tournament module is already tucked into src/test/resources/firebase/game/deploy for your convenience, but if you make changes to it, you'll need to copy the target/tournament-tutorial-1.0-SNAPSHOT.tar file to that location.

But please note that building tournaments are an inherently complex endeavor. Going through this sample might take you an hour or so, but will be time well invested. Many people (of average intelligence or higher) have tried to create tournaments and failed. Using the tournament support in Firebase, your chances of succeeding are much higher.

Here's a screenshot of the client showing the tournament lobby:

file:tournamentclient.png

Creating the Tournament Archive

If you want to do this from scratch, start with executing:

mvn archetype:generate -DarchetypeGroupId=com.cubeia.firebase.tools -DarchetypeArtifactId=firebase-tournament-archetype -DarchetypeVersion=1.8.0 -DarchetypeRepository=http://m2.cubeia.com/nexus/content/groups/public

From the terminal, answer the questions and you're good to go.

Flow of a Tournament

Before we start, let's look at what steps are normally required for a tournament to run and which component is responsible for each step.

  1. Create tournaments - done via MttFactory.createMtt() from MttActivator.
  2. Open registrations phase - game dependent, typically done by setting LobbyAttributes.
  3. Handle registrations - done in PlayerInterceptor (called before registration is complete) and PlayerListener (called after registration is complete).
  4. Create tables - done via MttTableCreator.createTables().
  5. Seat players - done via MTTSupport.seatPlayers().
  6. Start game rounds at tables - done via MTTSupport.sendRoundStartActionToTable().
  7. Send round reports to tournament from tables - done via TournamentNotifier.sendToTournament.
  8. Handle round reports - done via removing players who are out and balancing tables: MTTSupport.unseatPlayers(), MTTSupport.movePlayer() and MTTSupport.closeTable().
  9. Finish and destroy tournament - done via MttFactory.destroyMtt() in the Activator.

The Tournament Activator

The archetype came with two files for you, the Activator and the Tournament. The activator is responsible for creating and destroying tournaments, so let's start there. To create a tournament, you call MttFactory.createMtt() from your Activator. The MttFactory will be handed to you via setMttFactory() before the activator is started, so store it into an instance variable. The start() method is a good place to actually create the tournaments. Here's what it might look like:

    public void start() {
        factory.createMtt(1, "tic-tac-toe-tournament-1", new TicTacToeCreationParticipant());
        factory.createMtt(1, "tic-tac-toe-tournament-2", new TicTacToeCreationParticipant());
        factory.createMtt(1, "tic-tac-toe-tournament-3", new TicTacToeCreationParticipant());
    }

This will create three tournaments. The first parameter is the tournamentLogicId which should correspond to the tournament-definition id in the tournament.xml file under src/main/resourcees. The second parameter is the name of the tournament and the third is a creation participant which will be called when the tournament is created and is then responsible for setting up further details, such as the lobby path and capacity of the tournament.

So, create a new file called TicTacToeCreationParticipant and implement it like so:

import com.cubeia.firebase.api.lobby.LobbyAttributeAccessor;
import com.cubeia.firebase.api.lobby.LobbyPath;
import com.cubeia.firebase.api.lobby.LobbyPathType;
import com.cubeia.firebase.api.mtt.MTTState;
import com.cubeia.firebase.api.mtt.activator.CreationParticipant;
import com.cubeia.firebase.api.mtt.support.MTTStateSupport;
import org.apache.log4j.Logger;

public class TicTacToeCreationParticipant implements CreationParticipant {

    private static final Logger log = Logger.getLogger(TicTacToeCreationParticipant.class);

    @Override
    public LobbyPath getLobbyPathForTournament(MTTState mtt) {
        LobbyPath lobbyPath = new LobbyPath(LobbyPathType.MTT, 1, "mtt", mtt.getId());
        log.info("Returning lobby path: " + lobbyPath);
        return lobbyPath;
    }

    @Override
    public void tournamentCreated(MTTState mtt, LobbyAttributeAccessor acc) {
        log.info("Tournament created " + mtt.getName() + " " + mtt.getId());
        MTTStateSupport state = (MTTStateSupport) mtt;
        state.setCapacity(4);
        state.setState(new TicTacTournamentState());
    }
}

So, what this does is just returning a default LobbyPath when that's requested and then once the tournament is created, we set the capacity of the tournament to 4 players. We also initialize the domain specific tournament state. This class is very simple and will be listed later.

At this point you'll want to add some dependencies to your pom.xml file:

        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.14</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>10.0</version>
        </dependency>
        <dependency>
            <groupId>com.google.code.gson</groupId>
            <artifactId>gson</artifactId>
            <version>2.1</version>
        </dependency>

We also need to talk a bit about that downcast of MTTState to MTTStateSupport. MTTState is an interface that needs to be implemented by the tournament. To make things easier, Firebase comes with a default implementation called MTTStateSupport. When Firebase creates the tournament, it will use this implementation, so we can safely downcast the state. It is possible to provide your own implementation of MTTState, but that's a fair amount of work and will not be covered in this tutorial.

Handling Registrations

The next step is to handle incoming player registrations to the tournaments. If this were a scheduled tournament, we would probably write some code to figure out when the tournament should open for registrations, given the start time. But now we are doing a sit&go, so registrations will be welcome from the beginning. When a player sends a registration request (MttRegisterRequestPacket), first the PlayerInterceptor.register() method will be called. If this method returns MttRegisterResponse.ALLOWED, Firebase will register the player and call PlayerListener.playerRegistered(). In this sample, we will keep the code easy, but in a real system, you would probably withdraw some funds from the player's account in the PlayerInterceptor.register() method. If this fails, you would instead respond with DENIED_LOW_FUNDS, for example. Putting it all together, our simple example implements the two interfaces in one class and looks like this:

import com.cubeia.firebase.api.mtt.MTTState;
import com.cubeia.firebase.api.mtt.MttInstance;
import com.cubeia.firebase.api.mtt.model.MttRegisterResponse;
import com.cubeia.firebase.api.mtt.model.MttRegistrationRequest;
import com.cubeia.firebase.api.mtt.support.MTTStateSupport;
import com.cubeia.firebase.api.mtt.support.registry.PlayerInterceptor;
import com.cubeia.firebase.api.mtt.support.registry.PlayerListener;
import org.apache.log4j.Logger;

public class TicTacToeRegistrationHandler implements PlayerListener, PlayerInterceptor {

    private static final Logger log = Logger.getLogger(TicTacToeRegistrationHandler.class);

    @Override
    public void playerRegistered(MttInstance instance, MttRegistrationRequest mttRegistrationRequest) {
        log.info("Player registered. Number of registered players: " + instance.getState().getRegisteredPlayersCount());
        if (instance.getState().getRegisteredPlayersCount() == 4) {
            startTournament(instance);
        }
    }

    private void startTournament(MttInstance instance) {
        log.info("instance.id: " + instance.getId() + " instance.state.id: " + instance.getState().getId());
        instance.getTableCreator().createTables(888, instance.getId(), 3, 2, "tic-tac-toe-tournament-table", null);
    }

    @Override
    public void playerUnregistered(MttInstance mttInstance, int i) {
        log.info("Player unregistered. Number of registered players: " + mttInstance.getState().getRegisteredPlayersCount());
    }

    @Override
    public MttRegisterResponse register(MttInstance mttInstance, MttRegistrationRequest mttRegistrationRequest) {
        log.info("Registering player " + mttRegistrationRequest.getPlayer().getPlayerId() + " to tournament " + mttInstance.getId());
        return MttRegisterResponse.ALLOWED;
    }

    @Override
    public MttRegisterResponse unregister(MttInstance mttInstance, int playerId) {
        log.info("Unregistering player " + playerId + " from tournament " + mttInstance.getId());
        return MttRegisterResponse.ALLOWED;
    }
}

You'll also want to go to the Tournament class and edit these two methods to look like this:

    public PlayerInterceptor getPlayerInterceptor(MTTStateSupport state) {
        return new TicTacToeRegistrationHandler();
    }

    public PlayerListener getPlayerListener(MTTStateSupport state) {
        return new TicTacToeRegistrationHandler();
    }

Delayed Registrations

In the case of scheduled tournaments, you'll probably want to prevent registrations until the registration phase begins. To do this, start by setting a LobbyAttribute called status (or whatever key you think is appropriate) to ANNOUNCED (or again, whatever you think feels right). Do this in the CreationParticipant. Now, when PlayerIntereceptor.register() is called, you check the status like this:

        if (!"REGISTERING".equals(instance.getLobbyAccessor().getAttribute("status").getStringValue())) {
            return MttRegisterResponse.DENIED; 
        }

Of course that can be de-uglified by using enums, but you get the point. Note that this is just an example, you don't need to put this code into the tic-tac-toe tournament.

Starting the Tournament

Now it's time to start the tournament! In our case, we will do that as soon as we have 4 players registered. There are two steps to it, you create the required tables and then you seat the players at those tables.

If you scroll back up to TicTacToeRegistrationHandler.playerRegistered() you'll see that I already snuck the code in there. The vital part is:

    private void startTournament(MttInstance instance) {
        instance.getTableCreator().createTables(888, instance.getId(), 3, 2, "tic-tac-toe-tournament-table", null);
    }

Here, 888 is the gameId of our game, in this case tic-tac-toe, 3 is the number of tables to create, 2 is the number of seats at each table, "tic-tac-toe-tournament-table" is the base name of the tables and we don't need to pass any data to the tables so the last parameter is null. In a more complex tournament, we might want to initialize the tables with some configurations, which we could pass as an Object via the last parameter.

Now, the tables will be created asynchronously and you will receive a callback when tables are created. There's a bit of a caveat in that you might get several callbacks, so you need to check if all the tables you requested are created before you move on. Here's what our method looks like:

    public void process(MttTablesCreatedAction action, MttInstance instance) {
        MTTStateSupport state = getState(instance);
        log.info("Tournament tables created for tournament: " + instance.getId() + " We now have: " + state.getTables().size() + " tables.");
        if (state.getTables().size() == 3) {
            int[] tables = Ints.toArray(state.getTables());
            TicTacTournamentState ticState = (TicTacTournamentState) state.getState();
            ticState.setFinalTableId(tables[2]);

            seatPlayers(state, tables);
            startTables(state, state.getTables());
        }
    }

So, we check if all table have been created and if so, we seat the players and tell the tables to start.

You might notice that we created 3 tables for our 4 players, which is one table more than we need. I did this so that we have the final table ready when we need it, in order to make the code simpler when we are moving players to the final table. Otherwise we would have to create the table and then wait for the callback, which would make the method above a bit messier. Obviously, there are many (possibly better) ways to skin this cat. We take note of the tableId for the last table and store that in our domain specific tournament state class, which looks like this:

import java.io.Serializable;

public class TicTacTournamentState implements Serializable {

    private int finalTableId;

    public void setFinalTableId(int tableId) {
        finalTableId = tableId;
    }

    public int getFinalTableId() {
        return finalTableId;
    }
}

Now, the startTables() method is an existing method in MTTSupport, but the seatPlayers() method needs to be defined:

    private void seatPlayers(MTTStateSupport state, int[] tables) {
        List<MttPlayer> shuffledPlayers = new ArrayList(state.getPlayerRegistry().getPlayers());
        Collections.shuffle(shuffledPlayers);

        /*
         * In this tournament, we have hard coded the number of players to be 4. Two matches are played and the
         * winner from each match will play each other in the final.
         */
        Collection<SeatingContainer> seating = new ArrayList<SeatingContainer>();
        for (int i = 0; i < shuffledPlayers.size(); i++) {
            MttPlayer player = shuffledPlayers.get(i);
            SeatingContainer container = new SeatingContainer(player.getPlayerId(), tables[i / 2]);
            log.info("Seating player " + container.getPlayerId() + " at table " + container.getTableId());
            seating.add(container);
        }
        seatPlayers(state, seating);
    }

As you can see we first shuffle the players and then place the first two players at the first table and the last two players at the second table.

Making the Game Support Tournaments

Now we will move from the tournament module to the game module. If you haven't done so already, please follow the game tutorial or just download the TODO:finished project. Then, you need to implement the following interfaces:

  1. MttAwareActivator - let the Activator implement this.
  2. TournamentProcessor - let the TicTacToe class implement this.
  3. TournamentTableListener - let the Listener class implement this.

Starting Rounds

In the process() method we saw earlier, we are sending a "start" signal to all the tables. So what we need to do on the game side is to let the TicTacToe class implement the TournamentProcessor interface. Our implementation of these methods looks like this:

    @Override
    public void startRound(Table table) {
        log.info("Starting round " + table.getId() + " in 3 seconds..");
        GameObjectAction action = new GameObjectAction(table.getId());
        action.setAttachment("start");
        table.getScheduler().scheduleAction(action, 3000);
    }

    @Override
    public void stopRound(Table table) {

    }

In our case, we are ignoring calls for stopping the round, because the tournament will never do that. Please note here that the intention is that the same game can run as an individual game or as part of a tournament. One key difference is that in the case of a tournament, the game needs to wait for the tournament to tell it to start a round and should not start rounds on its own initiative. In the tic-tac-toe sample, we start the game when the second player sits down, in the playerJoined method. This method is not invoked in the case of a tournament, so we don't need to change it. However, we should make that class (called Listener) implement TournamentTableListener as well. We actually won't do anything in any of those methods, so just go ahead and let your IDE fill in default implementations.

The other place where we tell the game to start is after it has finished (in the progress() method) and we need to change that. The new method will look like:

    private void progress(Table table, Board board, Winner winner, int playerId) {
        GameData data;
        if (winner == NONE) {
            data = createGameData(board, "act");
            data.pid = board.getPlayerToAct();
        } else if (winner == Winner.TIE) {
            data = createGameData(board, "tie");
            scheduleNewGame(table);
        } else {
            data = createGameData(board, "win");
            data.pid = playerId;
            if (isTournamentTable(table)) {
                sendRoundReport(table, playerId);
            } else {
                scheduleNewGame(table);
            }
        }
        notifyAllPlayers(table, data);
    }

    protected boolean isTournamentTable(Table table) {
        return table.getMetaData().getType() == TableType.MULTI_TABLE_TOURNAMENT;
    }

Above I also included the isTournamentTable() method.

Sending Round Reports

As you can see, if the game is part of a tournament, we will send a round report instead of scheduling a new game when it has finished. A round report tells the tournament that a round has finished. In our case, we don't need to tell the tournament when there's a draw, in that case we just keep on playing until eventually we get a winner. However, in a poker tournament you usually send a round report after each hand, because the tournament might want to balance the tables by moving a player from your table to a table with fewer players, and between hands is a good time for doing that. The round report can contain arbitrary data, and in our case all we need to do is tell the tournament who won the game:

    private void sendRoundReport(Table table, int winner) {
        MttRoundReportAction roundReport = new MttRoundReportAction(table.getMetaData().getMttId(), table.getId());
        roundReport.setAttachment(winner);
        log.info("Sending round report where winner is " + winner + " mttId is " + roundReport.getMttId() + " and tableId is " + roundReport.getTableId());
        table.getTournamentNotifier().sendToTournament(roundReport);
    }

Believe it or not, but these are all the changes we need to do on the game side. Let's jump back to the tournament and handle those round reports.

Handling Round Reports

So, now we are back in the tournament module. Open the Tournament class and implement the process(MttTablesCreatedAction action, MttInstance instance) method like so:

    public void process(MttRoundReportAction action, MttInstance instance) {
        log.info("Received round report. Attachment: " + action.getAttachment());
        MTTStateSupport state = getState(instance);

        // Get the winner and the loser.
        int winnerId = (Integer) action.getAttachment();
        int loserId = getLoserId(action.getTableId(), winnerId, state);

        // Unseat the players.
        unseatPlayers(state, action.getTableId(), Arrays.asList(loserId), Reason.OUT);
        unseatPlayers(state, action.getTableId(), Arrays.asList(winnerId), Reason.BALANCING);

        // Remove the loser from the tournament and inform him
        state.getPlayerRegistry().removePlayer(loserId);
        sendAction("tournament-lose", instance, loserId);

        // If there's only one player left, we have a winner!
        if (state.getPlayerRegistry().size() == 1) {
            sendAction("tournament-win", instance, winnerId);
            instance.getLobbyAccessor().setAttribute("status", AttributeValue.wrap("finished"));
        } else {
            // Seat the winner at the final table.
            TicTacTournamentState ticState = (TicTacTournamentState) state.getState();
            int finalTableId = ticState.getFinalTableId();
            SeatingContainer container = new SeatingContainer(winnerId, finalTableId);
            seatPlayers(state, asList(container));
            log.info("Players at final table: " + state.getPlayersAtTable(finalTableId).size());
            if (state.getPlayersAtTable(finalTableId).size() == 2) {
                sendRoundStartActionToTable(state, finalTableId);
            }
        }
    }

    private int getLoserId(int tableId, int winnerId, MTTStateSupport state) {
        Collection<Integer> playersAtTable = state.getPlayersAtTable(tableId);
        playersAtTable.remove(winnerId);
        return playersAtTable.iterator().next();
    }

    private void sendAction(String action, MttInstance instance, int playerId) {
        instance.getMttNotifier().notifyPlayer(playerId, createAction(instance, playerId, new TournamentData(action)));
    }

    private MttDataAction createAction(MttInstance tournament, int playerId, TournamentData data) {
        MttDataAction action = new MttDataAction(tournament.getId(), playerId);
        Gson gson = new Gson();
        String json = gson.toJson(data);
        action.setData(wrap(json.getBytes()));
        return action;
    }

There's a bit more to chew here, but it should be fairly straightforward. The one trick is how we get a hold of the final table. Again, we could've opted to create the final table on the fly, but since table creations are asynchronous, we would then have to move the seating of players to the table created method.

You'll need the TournamentData class as well. It looks like this:

public class TournamentData {
    String action;

    public TournamentData(String action) {
        this.action = action;
    }

    public String getAction() {
        return action;
    }

    public void setAction(String action) {
        this.action = action;
    }
}

Destroying the Tournament

The final step is of course to destroy the tournament. If you read the process(MttRoundReportAction action.. method carefully, you noticed that we did something when the tournament is finished:

instance.getLobbyAccessor().setAttribute("status", AttributeValue.wrap("finished"));

This is a way of telling the activator that the tournament can safely be removed. On the activator side, we'll need to check the the tournaments regularly, so add this in the start method:

       scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
       scheduledExecutorService.scheduleAtFixedRate(this, 1, 1, TimeUnit.MINUTES);

You'll also want to add a private ScheduledExecutorService scheduledExecutorService; instance variable and let the Activator implement Runnable. The run() method looks like this:

    @Override
    public void run() {
        log.info("Checking tournaments.");
        MttLobbyObject[] mttLobbyObjects = factory.listTournamentInstances();
        for (MttLobbyObject tournament : mttLobbyObjects) {
            AttributeValue status = tournament.getAttributes().get("status");
            if (status != null && "finished".equals(status.getStringValue())) {
                log.info("Found finished tournament, destroying..");
                factory.destroyMtt(888, tournament.getTournamentId());
            }
        }
    }

Note that the first parameter to the destroyMtt method is the gameId of the game that this tournament was playing. This is used to shut down any remaining tables.

In a more realistic scenario, you'll probably want to schedule these things. When a tournament ends, the winner typically wants to sit at the table and gloat for a bit, so it might be mean to shut down the table immediately. Also, players might want to log in just to see who won the tournament, and it won't show up in the lobby if it's been destroyed, so at least for bigger tournaments it's common to let it stick around for a number of hours or even days.

Finally, we'll shut down the executor in the stop() method:

    public void stop() {
        scheduledExecutorService.shutdown();
    }

This concludes what we need to do on the server side. But we still can't play any tournaments unless we have a client that supports it. So, we'll move on to the client side.

Client Side Changes

All that remains to do now is to implement the changes on the client side. We will take the tic-tac-toe JavaScript client and modify it to support tournaments. It turns out that most of the work is around showing the "tournament lobby", meaning the list of tournaments and creating the register / unregister feature.

If you haven't already, head over to the tic-tac-toe JavaScript client tutorial and grab the code. We will now modify it for our needs.

Subscribing to Tournaments

The first thing to do is to subscribe to tournaments instead of tables. Change the loginCallback function like so:

function loginCallback(status, playerId, name) {
    console.log("Login status " + status + " playerId " + playerId + " name " + name);
    myPlayerId = playerId;
    message("Login " + status + '</p>');
    if (status === 'OK') {
        console.log('Login OK, subscribing to lobby.');
        createGrid();
        message("Subscribing to tournament lobby ");

        // Subscribe to tournaments..
        var subscribeRequest = new FB_PROTOCOL.LobbySubscribePacket();
        subscribeRequest.type = FB_PROTOCOL.LobbyTypeEnum.MTT;
        subscribeRequest.gameid = 1;
        subscribeRequest.address = "/";
        connector.sendProtocolObject(subscribeRequest)
    }
}

This creates a subscription request subscribing to the entire tree of tournaments (the "/" means the root of the lobby tree). The gameid 1 corresponds to the tournamentLogicId we chose back at the beginning of this tutorial.

Now, we need to handle the tournament lobby packets. Change the lobbyCallback function to:

function lobbyCallback(protocolObject) {
    console.log("Received lobby packet: " + protocolObject + " classid: " + protocolObject.classId);
    switch (protocolObject.classId) {
        case FB_PROTOCOL.TournamentSnapshotListPacket.CLASSID:
            console.log("Handle tournament snapshot list");
            handleTournamentSnapshotList(protocolObject.snapshots);
            updateRegisterButtons(registeredTournaments);
            break;
        case FB_PROTOCOL.TournamentUpdateListPacket.CLASSID:
            console.log("Handle tournament update list");
            handleTournamentUpdateList(protocolObject.updates);
            break;
    }
}

A tournament snapshot list packet contains a list of tournament snapshot and a tournament update list is a list of tournament updates. The difference between a snapshot and an update is that a snapshots contains all data about an tournament while a snapshot only contains delta changes.

Handling Tournament Lobby Data

We handle the snapshots and updates like this:

function handleTournamentSnapshotList(tournamentSnapshotList) {
    for (var i = 0; i < tournamentSnapshotList.length; i++) {
        handleTournamentSnapshot(tournamentSnapshotList[i]);
    }
    jQuery("#lobbyList").trigger("reloadGrid");
}

function handleTournamentSnapshot(tournamentSnapshot) {
    var params = tournamentSnapshot.params;
    var name = getStringParam('NAME', params);
    var registered = getIntParam('REGISTERED', params);
    var capacity = getIntParam('CAPACITY', params);
    var i = lobbyData.push({id:tournamentSnapshot.mttid, name:name, capacity:capacity, registered:registered});
    jQuery("#lobbyList").jqGrid('addRowData', tournamentSnapshot.mttid, lobbyData[i - 1]);
}

function handleTournamentUpdateList(updates) {
    console.log("Handling tournament update list.")
    for (var i = 0; i < updates.length; i++) {
        var update = updates[i];
        var params = update.params;
        var tournament = findTournament(update.mttid);
        console.log("Updating tournament " + update.mttid + " params: " + params);
        tournament.registered = getIntParam('REGISTERED', params);
        console.log("Updating registered for " + tournament.id + " to: " + tournament.registered);
        jQuery("#lobbyList").jqGrid('setRowData', tournament.id, {registered:tournament.registered});
    }
}

This is of course a bit flawed, since we only care about one possible delta change, namely the number of registered players. In this tutorial, we know that's all that's going to change, so it's OK. Fetching the parameter values from these packets is a bit tricky, since they are base64 encoded, so here's the code for doing that:

function getStringParam(key, params) {
    return getParam(key, params, "string");
}

function getIntParam(key, params) {
    return getParam(key, params, "int");
}

function getParam(key, params, type) {
    for (var i = 0; i < params.length; i++) {
        var param = params[i];
        if (param.key == key) {
            var byteArray = FIREBASE.ByteArray.fromBase64String(param.value);
            var value;
            if (type == "string") {
                value = utf8.fromByteArray(byteArray);
            }
            else if (type == "int") {
                value = intFromBytes(byteArray);
            }
            console.log("returning value " + value + " for param " + key);
            return value;
        }
    }
    console.log("returning null for param " + key);
    return null;
}

function intFromBytes(x) {
    var val = 0;
    for (var i = 0; i < x.length; ++i) {
        val += x[i];
        if (i < x.length - 1) {
            val = val << 8;
        }
    }
    return val;
}

Showing the Tournament Lobby

OK, so we can now get the tournament list, but we need to display it as well. Change the createGrid functions like so:

function createGrid() {
    $("#lobbyList").jqGrid({
        datatype:"local",
        data:lobbyData,
        height:204,
        colNames:['Name', 'Capacity', 'Registered', ''],
        colModel:[
            {name:'name', index:'name', width:250, sorttype:"string"},
            {name:'capacity', index:'capacity', width:110, sorttype:"int"},
            {name:'registered', index:'registered', width:110, sorttype:"int"},
            {name:'act', index:'act', width:100}
        ],
        caption:"Lobby",
        scroll:true,
        multiselect:false,
        gridComplete:function () {
            var ids = jQuery("#lobbyList").jqGrid('getDataIDs');
            for (var i = 0; i < ids.length; i++) {
                var mttId = ids[i];
                registerButton = "<input class='ui-button' type='button' value='Register' onclick='register(" + mttId + ");'/>";
                jQuery("#lobbyList").jqGrid('setRowData', ids[i], {act:registerButton});
            }

        },
        cellSelect:function () {
        }

    });
    console.debug("grid created");
}

Handling Registrations and Unregistrations

The onClick function above refers to a function that will send a registration request. Here's the code for registration and unregistration:

function register(mttId) {
    console.log("Registering to tournament " + mttId);
    message("Registering to tournament " + mttId);
    var registerRequest = new FB_PROTOCOL.MttRegisterRequestPacket();
    registerRequest.mttid = mttId;
    connector.sendProtocolObject(registerRequest);
}

function unregister(mttId) {
    console.log("Unregistering from tournament " + mttId);
    message("Unregistering from tournament " + mttId);
    var unregisterRequest = new FB_PROTOCOL.MttUnregisterRequestPacket();
    unregisterRequest.mttid = mttId;
    connector.sendProtocolObject(unregisterRequest);
}

Now, we need to listen to the registration responses, so we can update the "Register" button to say "Unregister" correctly. In your packetCallbac function, add this after the last case:

        case FB_PROTOCOL.MttRegisterResponsePacket.CLASSID:
            console.log("Registration response status: " + packet.status);
            handleRegisterResponse(packet);
            break;
        case FB_PROTOCOL.MttUnregisterResponsePacket.CLASSID:
            console.log("Unregistration response status: " + packet.status);
            handleUnregisterResponse(packet);
            break;
        case FB_PROTOCOL.NotifyRegisteredPacket.CLASSID:
            console.log("Notify Registered");
            registeredTournaments = registeredTournaments.concat(packet.tournaments);
            console.log("Registered tournaments: " + registeredTournaments);
            break;

The NotifyRegistered packet is received when you log in and tells you which tournaments you are already registered to. The corresponding handle functions you need are:

function handleRegisterResponse(packet) {
    if (packet.status == "OK") {
        registeredTournaments.push(packet.mttid);
        updateRegisterButtons(registeredTournaments);
    }
}

function handleUnregisterResponse(packet) {
    if (packet.status == "OK") {
        removeRegisteredTournament(packet.mttid);
        updateRegisterButtons(registeredTournaments);
    }
}

function updateRegisterButtons(registeredTournaments) {
    console.log("Updating register buttons.");
    var ids = jQuery("#lobbyList").jqGrid('getDataIDs');
    for (var i = 0; i < ids.length; i++) {
        var mttId = ids[i];
        var tournamentData = findTournament(mttId);
        if (tournamentData) {
            var registerButton;
            if (registeredTournaments.indexOf(parseInt(mttId)) != -1) {
                registerButton = "<input class='ui-button' type='button' value='Unregister' onclick='unregister(" + mttId + ");'/>";
            } else {
                registerButton = "<input class='ui-button' type='button' value='Register' onclick='register(" + mttId + ");'/>";
            }
            tournamentData.act = registerButton;
            jQuery("#lobbyList").jqGrid('setRowData', mttId, {act:registerButton});
        }
    }
}

function findTournament(tournamentId) {
    for (var i = 0; i < lobbyData.length; i++) {
        var object = lobbyData[i];
        if (object.id == tournamentId) {
            return object;
        }
    }
    return null;
}

Opening the Tournament Table

OK, so now we need to make sure that we will open a table when the tournament starts. Do this by adding another case in the above packetCallback function:

        case FB_PROTOCOL.MttSeatedPacket.CLASSID:
            console.log("MttSeatedPacket.");
            handleMttSeated(packet);
            break;

And then:

function handleMttSeated(packet) {
    console.log("I've been seated at table " + packet.tableid + " in tournament " + packet.mttid);
    connector.joinTable(packet.tableid, -1);
    myTableId = packet.tableid;
    $('#lobby').hide();
    $('#table').show();
}

Handling Tournament Messages

The last thing to do is to react when the tournament tells us that we won or lost the tournament. This means we must handle the MttTRansportPacket:

        case FB_PROTOCOL.MttTransportPacket.CLASSID:
            console.log("Received mtt data.");
            handleMttTransport(packet);
            break;

Followed by:

function handleMttTransport(packet) {
    var byteArray = FIREBASE.ByteArray.fromBase64String(packet.mttdata);
    var message = utf8.fromByteArray(byteArray);
    gameMessage("Received " + message + " from mtt " + packet.mttid)

    handleTournamentData(jQuery.parseJSON(message));
}

function handleTournamentData(json) {
    if (json.action == "tournament-lose") {
        alert("You are out of the tournament!");
    } else if (json.action == "tournament-win") {
        alert("You won the tournament. Congratulations!");
    }
}

Running the Tournament

In order to test this out, first do mvn install in the tournament module. Now open the game POM file and add the following dependency:

<dependency>                                                                                                                      
  <groupId>com.cubeia.tutorial.tictactoe.tournament</groupId>                                                                       
  <artifactId>tournament-tutorial</artifactId>                                                                                      
  <version>1.0-SNAPSHOT</version>                                                                                                   
  <type>firebase-tar</type>                                                                                                         
  <scope>provided</scope>                                                                                                           
</dependency>

Now start Firebase by running mvn package firebase:run in the game-module (you just made the game module depends on the tournament module, so both the gar and the tar file will be placed in Firebase' deploy folder). Then open the index.html file in the client and log in with four players (using an integer as the password, which will be the playerId).

Again, you can download the game module here, the tournament module here and the client here.

Conclusion and Known Limitations

This marks the end of probably the longest tutorial on Firebase. If you followed it all the way through, I congratulate you! :) Feel free to contact us if you have any questions.

When you get this running, you'll notice that there are a couple of things missing, mostly on the client side:

  1. You can't go back to the lobby once you are at a table
  2. When your table is closed, you won't know and won't get back to the lobby
  3. The register/unregister button should be disabled once the tournament is running

These are minor things that are left for the reader as an exercise.. :)

Good luck!


Footnotes
1A sit&go is a tournament that starts as soon as there are enough players.
2A heads-up tournament means that players are playing one versus one. For more on this see Wikipedia's article on single-elimination tournaments.

Personal tools