Tutorial

Purpose of this tutorial is to familiarize you with the timeline functionality of the Pogamut, what is it and how to use it. In order to do that, you will first create a bot that will try to capture the flag in the CTF game of UT2004. After finishing the bot, the functionality of timeline will be demonstrated using the bot. To open up this example in NetBeans follow up the steps in Opening Pogamut Examples chapter (if the archetype is not present, follow adding new Pogamut example project guide). This archetype information is below.

For UT2004 example:

After opening the example, you can continue to the second part of the tutorial.

At the end of the tutorial, you will have CTF bot that is providing additional data to the Pogamut Netbeans plugin that makes it easier to know what, where, and when was the CTF bot doing.

Rules of Capture The Flag game

Before we even start thinking about creation of a bot, we have to know about the CTF game rules, that are little more complicated than free-for-all DeathMatch.

CTF is a two-team game, where each team tries to score points by taking enemy flag and carrying it to its team base. Rules of the Capture The Flag game:

  • There are at least two teams, each team have flag and home base.

  • At the start of the game, each flag is located at the respective There are two teams, each team have flag and home base.

  • Every bot belongs to one team

  • At the start of the game, flags are located at their respective flag base.

  • Team member can pick the enemy flag by running over it.

  • If team member carrying enemy flag is killed, the flag is dropped.

  • If team member runs over its team dropped flag, the flag returned to its home base.

  • Team scores, when team member carries enemy flag to its home base, while its team flag is at home base.

  • Game ends when it is over specified time limit (posted in GameInfo message) or if at least one team achieves score higher than GoalTeamScore (also from GameInfo message).

Team selection and team balancing

Every bot can specify the team it belongs to in a team attribute of the INIT message that is send during handshake of the bot with the Unreal. The INIT message supplies data necessary for creation of the bot's avatar in the game (such as initial location, name, skin or team) and GameBots creates the avatar upon recieving. Bot can get total number of teams in the game from attribute MaxTeams of GameInfo message.

    @AgentScoped
    public class Hunter extends UT2004BotModuleController<UT2004Bot> {
        // ...
        @Override
        public Initialize getInitializeCommand() {
            int maxTeams = this.game.getMaxTeams();
            return new Initialize().setName("NavigationBot").setTeam(1);
        }
        // ...
    }
                
                

Valid team number is from 0 to GameInfo.MaxTeams - You should also note, that team's size is not unlimited, the maximal size of each team is GameInfo. MaxTeamSize. If the team is not specified in the INIT message, GameBots will use team number 255 in DeathMatch and team number 0 in CTF games.

In some situations, you may want to select team of a bot based on number of players per team or some other metric thus balancing number of players in teams. This is useful in many situations, such as running same bot program multiple times and still creating two teams with same number of players (one team may have one player more than the other) or if you want your bot to join the team that has lower score and many other situations.

For further details, consult GameBots documentation.

Information about flags and bases

In CTF game, information about flags is send as synchronous message from the GameBots, so the bot always have up-to-date info about flags. You can retrieve current info from the worldview, e.g. like this:

    public FlagInfo getFlag(int team) {
        Collection<FlagInfo> flags = bot.getWorldView().getAll(FlagInfo.class).values();
        for (FlagInfo flagInfo : flags) {
            if (flagInfo.getTeam() == team) {
                return flagInfo;
            }
        }
        throw new IllegalStateException("FlagInfo about with seeked team not found.");
    }
                    
                    

You'll get location and possibly holder if you can see the flag (and possibly its carrier), other information is always available. FlagInfo. getState() is probably most useful.

Unfortunately, you can't directly query the worldview for info about the team base, but we know that base is located at navpoint with a specific id. Following code is showing how to find the home for the specified team:

    // unreal assigns names to teams by default, team 0 is red, 1 is blue, 2 is green and 3 is gold
    private static String[] teamColors = new String[] {"Red", "Blue", "Green", "Gold"};

    public NavPoint getTeamBase(int team) {
        String flagBaseStr = "x" + teamColors[team] + "FlagBase0";

        for (NavPoint navpoint : bot.getWorldView().getAll(NavPoint.class).values()) {
            if (navpoint.getId().getStringId().contains(flagBaseStr)) {
                return navpoint;
            }
        }
        throw new IllegalStateException("Unable to find base for " + teamColors[team] + " team.");
    }
                    
                    

CTF bot schema

We are going to create our simple CTF bot using FSM(Finite State Machine), so before we start programming anything, it is necessary to specify how the bot should make decisions. You should always start with planning architecture of a bot's logic, it usually saves a lot of trouble and time. We are going to use work in progress library for FSMs that is currently being developed.

The bot has three different state types and multiple transitions. Bot starts at the hunt state, so it should wander around map, collect add-ons and kill enemies. When he sees enemy's flag he switches to pickup enemy flag and picks it up, and after he picks it up, he switches into go home. Unfortunately, team flag may be missing, because enemy bot got to it, so bot can switch into hunt mode and hoping to find enemy and kill him (NOTE: that won't return flag home, but if level is small enough, there is a good chance bot will accidently move over the dropped flag while in hunt mode). After flag has been delivered home, go back to the hunt mode. The rest of transitions is for events, when something doesn't go in the expected way, e.g. transition from hunt to go home is for case, when bot accidently moves over enemy flag, that he couldn't see before.

As you are aware, FSM consists of list of states, start state and transitions between states. Start state is the state "Hunt," bot will switch into that state when he enters the environment or when he is killed.

States used in the FSMBot library are not exactly states in classical FSM definition, but more like activities the bot is supposed to do. List of states:

  • Hunt - in this state bot should behave like hunter, kill members of enemy team and collect weapons, and other add-ons.

  • Pick up enemy flag - go to the place where is the enemy's flag and pick it up. This state expects that bot knows where the enemy's flag is (e.g. bot sees it).

  • Go home - return to home base. This state should be used when bot holds enemy's flag.

There are many shortcoming, e.g. state Hunt should preferably shoot at enemy holding our flag or state "Pick up enemy flag" and "Go home" should shoot at visible enemy while racing for their destination.

Transitions are a little tricky, basically there are two types of transitions that differs by eligibility (under what circumstances are they considered for potential transition from state A to B).

  • Sequential transition - is eligible only after state has finished(e.g. if bot is supposed to move somewhere, the sequential transition won't be eligible until bot is at the destination). In order for it to be eligible, source state method isFinished must return true.

  • Interrupt transition - is always eligible and if triggered, it interrupts the activity the state was doing, e.g. if bot is supposed to move somewhere, interrupt transition is eligible while bot is moving and after state has finished.

FSM State flow

In order to properly utilize FSMBot library, you need to have an idea how do states look like, how to program them and how are they processed. Every state has to implement interface IFSMState. Careful, there is an interface with the same name in afsm library, that is used during communication with GameBots.

There are basically two paths state can go:

  • State is not interrupted by interrupt transition during its execution:

    1. init
    2. loop method isReady , until it returns true
    3. if isFinished returns true
      1. cleanup
      2. loop method isCleaned, until it returns true
      3. The state has been finished, for now and FSM will start to execute another
    4. run
    5. Go to point 2
  • State is interrupted by interrupt transition:

    1. init
    2. loop method isReady , until it returns true
    3. if FSM detects eligible interrupt transition that can be triggered
      1. interrupt
      2. loop method isInterrupted, until it returns true
      3. The state has been interrupted, FSM will start to execute another
    4. run
    5. Go to point 3

I attemp to make sure that destination state of transition sees the environment same way the transition did, so the condition that triggered the transition itself is still valid even in a destination state (e.g. if transition is supposed to trigger when visible player appears, the destination state will see player at the same position and still visible). This behavior is not guaranteed if state has to loop over any of following methods:

  • isReady
  • isInterrupted
  • isCleaned

Creating the project

In this section we will create the project and set up dependencies necessary for us to build FSM bot.

  1. Start up Netbeans
  2. Once Netbeans are loaded, create a new project:
    1. Choose FileNew Project (Ctrl+Shift+N). Under category Pogamut UT2004 select Java Bot Project and click Next.
    2. In the Name and Location panel, type CTFBot in the Project Name. Choose Project Location to appropriate directory on your computer, such as c:/development. Click Finish.
    The IDE will create and open new project CTFBot with one package ctfbot and default empty bot.
  3. Add FSMBot library as library to your project:

    • Open properties of CTFBot project

    • In Libraries, select tab Compile and click on Add Library....
    • In the file dialog find the "Pogamut FSMEngine" library, select it and click on Add Library.

      If you can't find the library in the list of available libraries, make sure that you have installed Netbeans plugin of Pogamut 3.1 or later.

Necessary infrastructure

Before we start with our translation of CTF bot schema into the states and transitions, we need to create some infrastructure, that will execute a FSM machine with our states.

Necessary infrastructure consists of

  • FSM itself - created and filled by createFSM()
                                
                                private IBotFSM createFSM() {
                                    BotFSM fsm = new BotFSM();
                                    // create states
                                    // e.g. IFSMState<CTFBot> exampleState= new StateEmpty();
    
                                    // add states
                                    // e.g. fsm.addState(exampleState)
    
                                    // add transitions
                                    // e.g. fsm.addTransition(new SomeCondition(), sourceState, targetState, 100, TransitionType.INTERRUPT);
    
                                    // set starting state of FSM
                                    // e.g. fsm.setInititialState(exampleState);
    
                                    return fsm;
                                }
                                
                            
    For now, this method only creates a FSM, but doesn't fill it with anything. We will add states and transitions later.
  • Context object - object that is shared among all states and transitions. Basically every method of state/transition is passed the context object. What is it good for? It can be used as a memory (e.g. how many times have you been to your rabbit hole), provider of Pogamut modules (like Senses, AgentInfo, CompleteBotCommandsWrapper) or anything else. In our case, we will use class CTFBot as context, but it is OK to use any object, no matter what. Class of context is used in generics of states and transitions, therefore if you see things like IFSMState<CTFBot> in this tutorial, that denotes fsm state interface with context object of type CTFBot. Always consult javadoc to be sure.
  • FSM executor - object that takes FSM and makes sure states are executed and transitions correctly change current state from one to another. FSMlib provides class BotFSMExecutor .
                                
                                private IBotFSMExecutor<CTFBot> fsmExecutor;
    
                                @Override
                                public void botInitialized(GameInfo info, ConfigChange config, InitedMessage init) {
                                    // Create FSM executor that will take care of calling methods of states and
                                    // about switching from one state to another.
                                    fsmExecutor = new BotFSMExecutor(createFSM(), getLog());
                                    // initialize fsm executor, parameter is context of fsm
                                    fsmExecutor.init(this);
                                }
                                @Override
                                public void logic() throws PogamutException {
                                    // make a step in execution of FSM, run statem check transitions, if necessary
                                    // switch from one state to another. Passed object is fsm context.
                                    fsmExecutor.step(this);
                                }
                                
                            

    BotFSMExecutor is instantiated in botInitialized, because it requires as a parameter a FSM, created by createFSM()) which in turn instantiates its states and transitions. Some states or transitions may require various modules during instantiation and such modules are initialized by the time bot reaches botInitialized. We also call init of BotFSMExecutor, that initialized initial state of FSM and the initial state is likely to use some method from some module.

    Method step makes single "step" in FSM, run states, check transitions, if necessary switch states and so on.

    As you can see, IFSMExecutor and IFSMState have generic type CTFBot, that is type of the context = shared object that is passed to all states. In our case, it is convenient to use CTFBot as the context, but it can be any object.

First state

With necessary infrastructure in place, lets create our very first state, that by itsefl won't do anything useful, think of it as foundation for all other states.

  • Create new package ctfbot.state where all states will be placed (context menu of package ctfbot under Source Packages of project CTFBot project → NewJava Package, type ctfbot.state into Package Name: and click on Finish).

  • Create class with name StateEmpty in package ctfbot.state.

                                    
                                    public class StateEmpty implements IFSMState<CTFBot> {
    
                                        public void init(CTFBot context) {
                                        }
    
                                        public boolean isReady(CTFBot context) {
                                            return true;
                                        }
    
                                        public void run(CTFBot context) {
                                        }
    
                                        public boolean isFinished(CTFBot context) {
                                            return false;
                                        }
    
                                        public void cleanup(CTFBot context) {
                                        }
    
                                        public boolean isCleaned(CTFBot context) {
                                            return true;
                                        }
    
                                        public void interrupt(CTFBot context) {
                                        }
    
                                        public boolean isInterrupted(CTFBot context) {
                                            return true;
                                        }
    
                                        public String getName() {
                                            return "Empty";
                                        }
                                    }
                                    
                                

    As you can see, this state isn't doing anything( init() and run() are empty), it is immediately initialized( isInitialized() returns true), cleaned and interrupted.

  • Add the StateEmpty to FSM initialization and set it as the initial state.

                                    
        private IBotFSM createFSM() {
            BotFSM fsm = new BotFSM();
            // create states
            IFSMState<CTFBot> empty = new StateEmpty();
    
            // add states
            fsm.addState(empty);
    
            // add transitions
            // nothing yet...
            // set starting state of FSM
            fsm.setInititialState(empty);
    
            return fsm;
        }
    
    
                                

Start up Unreal server and build and run CTFBot project, bot should appear in the game and last line in the Output window of Netbeans should contain following text: (CTFBot1) [FINE] 18:02:58.791 <User> State "Empty" is being initialized.

StateHunt

Although we finally have working FSM bot, it is not a very useful one. Let's create new state, StateHunt from our specification. Just to remind you, bot in the state Hunt should do following:

  • When bot sees enemy player and has ammo, shoot the enemy player
  • Else (no enemy/no ammo) stop shooting and go to some random NavPoint in game with spawned item.

That is easy.

  • Create new class StateHunt in package ctfbot.state and have it implement interface IFSMState<CTFBot> .
  • We will want to use some modules for this state: Players , SimpleShooting , AgentInfo , Items , some PathPlanner and PathExecutor . Also, collection of all NavPoint objects will be useful. We could instantiate modules in the state, but that would be wasting of memory, so we will take them from context object. Add getters for the modules into the CTFBot (our context class).
        public SimpleShooting getShooting() { return shoot; }
        public Players getPlayers() { return players; }
        public AgentInfo getAgentInfo() { return info; }
        public PathPlanner getPathPlanner() { return pathPlanner; }
        public PathExecutor getPathExecutor() { return pathExecutor; }
        public Items getItems() { return items; }
        public Collection<NavPoint> getNavPoints() { return getWorldView().getAll(NavPoint.class).values(); }                             
                            
  • Create a initialization for StateHunt , in this case, we don't need to do anything. Also, remember to set name of state.
                                
        public void init(CTFBot context) {}
        public boolean isReady(CTFBot context) { return true; }
        public String getName() { return "Hunt"; }\                                
                            
    In cases when the state is supposed to do one thing and be immediately finished, then all necessary code can be put into init() . States like "Shoot burst" or "jump" would be candidates for such approach.
  • Implement method run() to make state behave according to specified targets(see above). Remember, this is called many times over execution of state, just like logic, it is therefore quite similar to what you would program into logic() method.
                         
        public void run(CTFBot ctx) {
            Map<UnrealId, Player> visibleEnemies = ctx.getPlayers().getVisibleEnemies();
    
            // do we see enemy and can we shoot?
            if (!visibleEnemies.isEmpty() && ctx.getAgentInfo().getCurrentAmmo() > 0) {
                ctx.getPathExecutor().stop();
                Player nearestPlayer = DistanceUtils.getNearest(visibleEnemies.values(), ctx.getAgentInfo().getLocation());
                ctx.getShooting().shoot(nearestPlayer.getId());
                return;
            }
            // if we got here, we have no enemy or no ammo, in either case, stop shooting
            if (ctx.getAgentInfo().isShooting()) {
                ctx.getShooting().stopShoot();
            }
            // we we currently moving somewhere? Like some spawned item or distant navpoint
            // if so, don't change destination, until movement is finsihed
            if (ctx.getPathExecutor().isMoving()) {
                return;
            }
            // we have no ammo or no enemy and we are standing at one place. That is waste of time, go somewhere and get stuff
            Location destination;
            Item randomSpawnedItem = MyCollections.getRandom(ctx.getItems().getSpawnedItems().values());
            if (randomSpawnedItem == null) {
                destination = MyCollections.getRandom(ctx.getNavPoints()).getLocation();
            } else {
                destination = randomSpawnedItem.getLocation();
            }
            ctx.getPathExecutor().followPath(ctx.getPathPlanner().computePath(destination));
         }
                                                            
  • Define when the state is finished, in our case, we are never finished, we want to hunt indefinitely, i.e. until some interrupt transition from the state is triggered.
                      
        public boolean isFinished(CTFBot context) {  return false; }                              
                            
  • Add routines for interrupt, i.e. routine that actually takes care of the state internals ( interrupt() ), and the other that says if interruption is finished ( isInterrupted() ). One of things we typically want to do, when the state is interrupted is to clean it. Actually, in most cases, we just default into cleanup mode, but in some bizzarre cases, it is necessary to differentiate between interruption and finishing.
                           
        public void interrupt(CTFBot context) {
            cleanup(context);
        }
    
        public boolean isInterrupted(CTFBot context) {
            return isCleaned(context);
        }        
                            
  • Implement cleanup routines. Our bot could be interrupted while shooting so stop shooting in that case and stop moving if the bot is moving. We don't do anything else, so cleanup is pretty straightforward and bot is changed into default neutral state (standing, not moving, not shooting). Cleanup is instant, so isCleaned doesn't have to indicate waiting.
                                
        public void cleanup(CTFBot ctx) {
            if (ctx.getAgentInfo().isShooting())
                ctx.getShooting().stopShoot();
    
            if (ctx.getPathExecutor().isMoving())
                ctx.getPathExecutor().stop();
        }
    
        public boolean isCleaned(CTFBot context) {
            return true;
        }
                            
  • Modify createFSM() in CTFBot to utilize StateHunt instead of StateEmpty

Feel free to test single-state hunt bot. Not perfect, of course, but it runs and kills. We could implement things like changing to weapon with ammo, either with some additional lines in run() or utilizing hierarchical sub FSM with states Hunt and ChangeToBestArmedWeapon and interrupt transitions HaveBetterWeapon and sequential transition True.

The ChangeToBestArmedWeapon would probably have all necessary code in init() and isFinished() would immediately return true, so sequential transition True would immediately occur.

"Pickup enemy flag" state

The demonstrated process is pretty much standard for all other states. Either program them yourself based on the requirements from CTF FSM schema or use provided sample. I would of course recommend programming them yourself, in order to familiarize yourself with FSMLib and Flag handling.

"Pickup enemy flag" is state that is trying to pickup enemy flag, that is visible by the bot on the ground(=enemy flag is in "dropped" or "home" state). State is finished, when bot loses sight of enemy flag (some obstacle hides it during movement to the flag...) or when someone picks the flag(bot, teammate).

StatePickupEnemyFlag has no initialization, and standard cleanup (stop moving). To the CTFBot add method getFlag, shown above.

    public void run(CTFBot ctx) {
        // do I still move along the path to the enemy flag?
        if (ctx.getPathExecutor().isMoving())
            return;

        // Go to enemy flag.
        int enemyFlagId = 1-ctx.getAgentInfo().getTeam();
        FlagInfo enemyFlag = ctx.getFlag(enemyFlagId);

        // enemy flag always have valid location, because it is visible (thx to isFinished running before run)
        PathHandle path = ctx.getPathPlanner().computePath(enemyFlag.getLocation());
        ctx.getPathExecutor().followPath(path);
    }

    public boolean isFinished(CTFBot ctx) {
        FlagInfo enemyFlag = ctx.getFlag(1-ctx.getAgentInfo().getTeam());

        // flag is visible and on the ground, state is not finished.
        boolean onGround = "Dropped".equalsIgnoreCase(enemyFlag.getState()) || "Home".equalsIgnoreCase(enemyFlag.getState());
        if (enemyFlag.isVisible() && onGround)
            return false;
        return true;
    }
                                

Method isFinished is ensuring that necessary conditions are still valid (enemy flag is still visible and on the ground) before run is called. Method run will repeatedly plan path to the enemy flag and go there. Why repeatedly? In theory, someone could pickup the flag and be immediately (few tens of ms) killed, therefore dropping enemy flag a little off its original position. The bot would go to the original flag position none the wiser and state would never end (because bot would see the flag in its new position).

Add the StatePickupEnemyFlag to the createFSM method of CTFBot, and, if you want to test it out, set the initial state to the instance of StatePickupEnemyFlag and spectate in the Unreal, how bot runs from its initial position to the enemy flag, if it can see it. If you are using startGamebotsCTFServer.bat to start the server (recommended), you are using map CTF-1on1-Joust, where bot starts at a position, where he can always see the enemy flag. If enemy flag is not visible or if your bot has picked the flag, state will end, because isFinished returns true. In such case, the Netbeans output window should contain message: (CTFBot1) [FINE] 14:13:47.494 <User> State "Pickup enemy flag" is finished.

You may have noticed that programing and especially testing individual states is far easier than in complex logic method, where you mingle enormous ammount if interactions.

"See enemy flag" condition

Now we have two states, but no way to change between them. In order to do that, we will have to add a transition (see the FSM schema). Our transition will be interrupt transition from StateHunt to StatePickupEnemyFlag, that will be triggered, when bot will see enemy flag.

Transitions are added using method addTransition of interface IBotFSM. That method accepts following parameters:

  • Condition(ICondition - method satisfied determines if the transition is elegible.
  • Source state(IFSMState<CONTEXT>) - determining the state from which the transition should happen.
  • Target state(IFSMState<CONTEXT>) - determining state to which the transition should happen.
  • Priority(int) - priority of transition. If there are multiple elegible transitions, all but the ones with highest priority will be discarded. For more detail look into FSMLib documentation.
  • Transition type (TransitionType) - determines if the transition is interrupt or sequential.

Modify createFSM() to create new transition.

        // add states
        fsm.addState(hunt);
        fsm.addState(pickupEnemyFlag);

        // add transitions
        // hunt-pickupEnemyFlag
        ICondition<CTFBot> seeDroppedEnemyFlag = new ICondition<CTFBot>() {
            public boolean satisfied(CTFBot ctx) {
                FlagInfo enemyFlag = getFlag(1-ctx.getAgentInfo().getTeam());
                boolean onGround = !"held".equalsIgnoreCase(enemyFlag.getState());
                return enemyFlag.isVisible() && onGround;
            }
        };
        fsm.addTransition(seeDroppedEnemyFlag, hunt, pickupEnemyFlag, 0, TransitionType.INTERRUPT);

        // set starting state of FSM
        fsm.setInititialState(hunt);                
                

We create ICondition as anonymous class, but feel free to put them into their own class file. If you look at FSM schema, you will see this transition there, with exactly the same properties as this one.

"Go home" state

Let's create the last state "Go home", that will find and execute path from current bot position to its base. The state is used when bots holds enemy flag and wants to get home in order to score a point. It will ignore enemy fire and all other distractions. State is finished, when bot arrives to its home base.

Use class name StateGoHome in ctfbot.state for the state. State has standard interrupt and cleanup methods (stop bot is necessary), but init and run are little different.

    public void init(CTFBot ctx) {
        NavPoint homeBase = ctx.getTeamBase(ctx.getAgentInfo().getTeam());
        PathHandle path = ctx.getPathPlanner().computePath(homeBase);

        ctx.getPathExecutor().followPath(path);
    }

    public boolean isReady(CTFBot ctx) {
        return ctx.getPathExecutor().isMoving();
    }

    public void run(CTFBot context) {
    }

    public boolean isFinished(CTFBot ctx) {
        return !ctx.getPathExecutor().isMoving();
    }
                                

In this case, we only need to get home and can therefore set up everything during initialization and no additional code is necessary in the run. The isReady makes us wait until the bot starts to move, if we hadn't waited until bot starts moving, the isFinished method would prematurely signal that state has been finished. That would result in StateGoHome finishing prematurely.

Finishing FSM

We have finally all states completed, transitions are sill missing. In this section, we will modify createFSM so it creates the FSM bot from our schema. First, make sure you have all states from schema instantiated and inserted into FSM:

    private IBotFSM createFSM() {
        BotFSM fsm = new BotFSM();
        // create states
        IFSMState<CTFBot> hunt = new StateHunt();
        IFSMState<CTFBot> pickupEnemyFlag = new StatePickupEnemyFlag();
        IFSMState<CTFBot> goHome = new StateGoHome();
        IFSMState<CTFBot> huntEnemyCarrier = new StateHunt();

        // add states
        fsm.addState(hunt);
        fsm.addState(pickupEnemyFlag);
        fsm.addState(goHome);
        fsm.addState(huntEnemyCarrier);

        ...
                

Notice that we create two different instances of state Hunt, the huntEnemyCarrier is used, when bot has flag, but his flag is not at home and he therefore can't score.

Now it is necessary to add the remaining transitions, rather boring and gruesome work, do not miss a transitions nor parameters.

        // add transitions
        // hunt-pickupEnemyFlag
        ICondition<CTFBot> seeDroppedEnemyFlag = new ICondition<CTFBot>() {

            public boolean satisfied(CTFBot ctx) {
                FlagInfo enemyFlag = getFlag(1 - ctx.getAgentInfo().getTeam());
                boolean onGround = !"held".equalsIgnoreCase(enemyFlag.getState());
                return enemyFlag.isVisible() && onGround;
            }
        };
        ICondition<CTFBot> someoneElseTookEnemyFlag = new ICondition<CTFBot>() {

            public boolean satisfied(CTFBot ctx) {
                FlagInfo enemyFlag = ctx.getFlag(1-ctx.getAgentInfo().getTeam());
                return !ctx.getAgentInfo().getId().equals(enemyFlag.getHolder());
            }
        };
        fsm.addTransition(seeDroppedEnemyFlag, hunt, pickupEnemyFlag, 0, TransitionType.INTERRUPT);
        fsm.addTransition(someoneElseTookEnemyFlag, pickupEnemyFlag, hunt, 100, TransitionType.INTERRUPT);

        // pickup enemy flag - go home
        ICondition<CTFBot> botHasEnemyFlag = new ICondition<CTFBot>() {

            public boolean satisfied(CTFBot ctx) {
                FlagInfo enemyFlag = ctx.getFlag(1-ctx.getAgentInfo().getTeam());
                return ctx.getAgentInfo().getId().equals(enemyFlag.getHolder());
            }
        };
        ICondition<CTFBot> always = new ICondition<CTFBot>() {

            public boolean satisfied(CTFBot context) {
                return true;
            }
        };
        fsm.addTransition(botHasEnemyFlag, pickupEnemyFlag, goHome, 100, TransitionType.NORMAL);
        fsm.addTransition(always, pickupEnemyFlag, hunt, 150, TransitionType.NORMAL);

        // hunt - go home
        ICondition<CTFBot> botLostEnemyFlag = new ICondition<CTFBot>() {

            public boolean satisfied(CTFBot ctx) {
                FlagInfo enemyFlag = ctx.getFlag(1-ctx.getAgentInfo().getTeam());
                return !ctx.getAgentInfo().getId().equals(enemyFlag.getHolder());
            }

        };
        fsm.addTransition(botHasEnemyFlag, hunt, goHome, 100, TransitionType.INTERRUPT);
        fsm.addTransition(botLostEnemyFlag, goHome, hunt, 100, TransitionType.INTERRUPT);

        // hunt enemy carrier - go home
        ICondition<CTFBot> teamFlagIshome = new ICondition<CTFBot>() {

            public boolean satisfied(CTFBot ctx) {
                FlagInfo teamFlag = ctx.getFlag(ctx.getAgentInfo().getTeam());
                return "home".equalsIgnoreCase(teamFlag.getState());
            }
        };
        ICondition<CTFBot> teamFlagIsNotHome = new ICondition<CTFBot>() {

            public boolean satisfied(CTFBot ctx) {
                FlagInfo teamFlag = ctx.getFlag(ctx.getAgentInfo().getTeam());
                return !"home".equalsIgnoreCase(teamFlag.getState());
            }
        };
        fsm.addTransition(teamFlagIshome, huntEnemyCarrier, goHome, 100, TransitionType.INTERRUPT);
        fsm.addTransition(teamFlagIsNotHome, goHome, huntEnemyCarrier, 100, TransitionType.INTERRUPT);

        // set starting state of FSM
        fsm.setInititialState(hunt);
                
                

In this case, we are using botHasFlag twice, but we can do that only becase it has no internal structures. Generally, you should always create new instance of ICOndition<CONTEXT>.

Try it out

That's all. You now have reasonably working CTF FSM bot. Start up Unreal server on some CTF map (e.g. startGamebotsCTFServer.bat), run bot and play against him, if you want.