Controller Tutorial - lg198/CodeFrayAPI GitHub Wiki
Example Controller
This mini-guide will outline the creation of an example controller, step by step.
Controller Class and Definition
First, create your controller class. This class will be instantiated once by the CodeFray Engine. Your controller's onRound(Golem g)
method will be invoked once per golem, per round.
For instance, imagine that there were 5 golems on your team. Every round, the golems are updated randomly. The calls to your onRound
method might look like the following, if you made your onRound print the id of the Golem
parameter:
Round 1:
onRound called, with golem{id=0}
onRound called, with golem{id=4}
onRound called, with golem{id=2}
onRound called, with golem{id=3}
onRound called, with golem{id=1}
Round 2:
onRound called, with golem{id=2}
onRound called, with golem{id=3}
onRound called, with golem{id=0}
onRound called, with golem{id=1}
onRound called, with golem{id=4}
The same golem will not be passed to onRound
more or less than once per round. It is your responsibility to keep track of the current round, as there is no indication of when rounds start or end.
Let's start the controller! Create the class for your controller. It should be in a package that describes the controller. You should use a reverse domain. For example, if your code is hosted on BitBucket, your username is bungaloboi, and your controller is called WackyCheeseController, you would place your controller class (WackyCheeseController.class) within the package com.bitbucket.bungaloboi.wackycheese
. Consequently, the id
for your controller, which will be addressed later, should be a variation of that package. In this example, the id
should be com.bitbucket.bungaloboi.WackyCheese
. Its not very different, and it doesn't have to be.
Create the class, and make sure it implements GolemController
:
package com.bitbucket.lrg10002.mycontroller;
//imports assumed
public class MyController implements GolemController {
public void onRound(Golem g) {
}
}
Next, you must add the ControllerDef
annotation to your class. Annotations are placed before a method, class, or state variable declaration, and are not terminated by a semicolon. They are preceded by @
. Replace the strings in the definition with ones that supply your information. For a detailed chart, look here.
package com.bitbucket.lrg10002.mycontroller;
@ControllerDef(
id = "com.bitbucket.lrg10002.MyController",
name = "My First Controller",
version = "1.0",
devId = "layneg")
public class MyController implements GolemController {
public void onRound(Golem g) {
}
}
To find your devId, check Slack :+1:.
Initialize that Sucker!
There's some stuff you might want to do before you instruct your golems to do anything. For instance, maybe you would want to know which team you are on... and it might be handy to store the opposing team too!
If CodeFray was a board game, these are things that you, the player, would know prior to starting. However, in CodeFray, there is no way to run code before the game starts. However, you can run code before you do anything with your first golem of the game. Its a no-brainer:
//all examples are assumed to be placed directly within the controller class body, unless otherwise noted
public boolean initialized = false;
public void onRound(Golem g) {
if (!initialized) {
//let's do stuff!
initialized = true;
}
}
This is a really simple concept: the first time onRound
is invoked, initialized
will be false. We then do whatever we need before we make any moves, then set initialized
to true so that we will never execute the code within the if statement again.
What will we do during initialization? Well, it helps to have two variables of type Team
, one for your team and the other for the team of the opponent. That way, you know who's blue and who's red without having to deduce it every time you need the information.
public boolean initialized = false;
public Team thisTeam, otherTeam;
public void onRound(Golem g) {
if (!initialized) {
thisTeam = g.getTeam();
otherTeam = myTeam == Team.RED ? Team.BLUE : Team.RED;
initialized = true;
}
}
We assign myTeam
to the team of the first golem that is passed through the onRound
method (we know its the first because this is the first time the method is being invoked). It is obviously your team. Next, we use a fancy bit of logic to assign the opposite team to otherTeam
. Since there are only two teams, if your team is red, then the other team must be blue. Similarly, if your team is not red, it must be blue, and thus the other team must be red. That line is equivalent to the following:
if (myTeam == Team.RED)
otherTeam = Team.BLUE;
else
otherTeam = Team.RED;
Who's Who?
Now that we are initialized, we need to control our golems. How will we do that, however, if we have no way of keeping track of what each golem is supposed to be doing? We need to check what kind of golem is being passed, and then act upon it.
You must have a strategy to begin programming the golems. Let's assume a simple strategy:
- If the golem is a runner, it should be going straight for the other flag. Once it reaches the flag, it should hightail it back to the win location
- If the golem is an assault golem, it should wander randomly, searching for the other team's golems
- If the golem is a defender, it should position itself near the flag and attempt to shoot any golems that approach
Obviously, this strategy can be expanded on greatly. For one, the amount of damage dealt on a golem depends on the distance between the shooter and the victim. Also, this leaves a lot of improvement as far as navigating the game board and detecting walls. However, it is a simple example that can be reflected in one class.
To start, let's check if the golem is a runner. If it is, let's attempt to move the golem closer to the opposing team's flag:
public boolean initialized = false;
public Team thisTeam, otherTeam;
@Override
public void onRound(Golem g) {
if (!initialized) {
thisTeam = g.getTeam();
otherTeam = thisTeam == Team.BLUE ? Team.RED : Team.BLUE;
initialized = true;
}
if (g.getType() == GolemType.RUNNER) {
onRoundRunner(Golem g);
}
}
private void onRoundRunner(Golem g) {
}
}
Everything that has to do with controlling a runner will now occur within the onRoundRunner method. Now, let's make the runner move towards the opposing team's flag. We will create a helper method, attemptMove(Golem g, Direction direct)
, to assist with some primitive pathfinding.
private void onRoundRunner(Golem g) {
if (g.getLocation().equals(g.getGame().getFlagLocation(otherTeam)) || g.isHoldingFlag()) {
boolean result;
do {
result = attemptMove(g, Direction.between(g.getLocation(), g.getGame().getWinLocation(thisTeam)));
} while (g.getMovesLeft() > 0 && result);
return;
}
boolean result;
do {
result = attemptMove(g, g.getFlagDirection(otherTeam));
} while (g.getMovesLeft() > 0 && result);
}
private boolean attemptMove(Golem g, Direction direct) {
Direction next = direct;
do {
if (g.canMove(next)) {
g.move(next);
return true;
}
next = next.clockwise();
} while (direct != next);
return false;
}
First, in the onRoundRunner
method, we check to see if the golem needs to go back to its win location - i.e. it has the flag and is ready to return to win the game. If it does, we attempt to move towards the win location. We will continue to try to move until we are out of moves or if we can't find any moves. Direction.between(Point a, Point b)
normalizes the vector between a and b and returns a cardinal direction that most closely represents said vector. After the if statement, we have a similar algorithm: it is simply attempting to move the golem to the other team's flag.
The attemptMove
method uses a very basic and somewhat questionable algorithm, in which it tries to go in the direction of the flag, but if it can't, it will try any other direction, checking each in a clockwise fashion. This is pretty ineffective. In fact, these don't even check if there is another golem or just a wall in front of them. If there's a golem, we should be shooting!
Speaking of which, we should shoot all the surrounding members of the other team, every round. Let's add that in a method!
private void shootAll(Golem g) {
g.search().stream().filter(gi -> gi.getTeam() == otherTeam).forEach(gi -> g.shoot(gi));
}
Well, wasn't that a short method! Let's look at it!
The method uses some new Java 8 features: streams and lambdas. If you don't know what those are, first read up on lambdas. For more info about streams, look here. Streams allow one to process many items by evaluating them one at a time. g.search()
returns a java.util.List of GolemInfo
objects, each of which represent a golem nearby. We don't want to shoot our teammates, so we need to filter our list to include only the golems not on our team. The filter
method of streams removes items from the stream if the lambda passed to filter
returns false. Thus, our stream now contains golems of the opposite team. Finally, we want to shoot each golem by passing every GolemInfo
instance to g.shoot
.
There is one problem with this method... each type of golem has a maximum number of shots per round. We need to make sure we don't exceed that number. Making that change is very simple - all we do is limit the stream to a certain number of results:
private void shootAll(Golem g) {
g.search().stream().filter(gi -> gi.getTeam() == otherTeam).limit(g.GetShotsLeft()).forEach(gi -> g.shoot(gi));
}