This is a tutorial which describes how to implement an AI for the programming game CodeCraft. You "play" CodeCraft by writing a program in Scala or Java and I am going to assume that you are already familiar with one of these languages. If you encounter a bug, have a question, or want to give feedback you can go on Gitter or send me an email at codecraftgame@gmail.com.
To get things set up, you need to create a new project and add the CodeCraft library as a dependency (in sbt: libraryDependencies += "org.codecraftgame" % "codecraft_2.11" % "0.6.0"
).
An implementation of the setup described in this section can be found in the folders scala-template and java-template.
The main entry point into the game is the TheGameMaster
object.
You can start the first level by calling TheGameMaster.runLevel1
as such:
import cwinter.codecraft.core.api._
object Main {
def main(args: Array[String]): Unit = {
TheGameMaster.runLevel1(new Mothership)
}
}
The same in Java:
import cwinter.codecraft.core.api.TheGameMaster;
class Main {
public static void main(String[] args) {
TheGameMaster.runLevel1(new Mothership());
}
}
The runLevel1
method expects an object of type DroneController
as argument.
This object will implement the AI for your initial unit.
So let's create a new file to implement this class:
import cwinter.codecraft.core.api._
class Mothership extends DroneController {
}
For Java, use the JDroneController
variant instead. It is essentially identical to DroneController
, but its getters return Java collections rather than their Scala counterparts:
import cwinter.codecraft.core.api.*;
class Mothership extends JDroneController {
}
As you can see, the Mothership class doesn't contain any code so your unit AI won't do anything yet. However, you should now be able to run the program. If everything works correctly, you will see a window that displays the game world. The camera is initially centered on your mothership, which looks like a septagon with various shapes inside (its modules, more on that later). The green pentagons are mineral crystals, which can be harvested to gain resources and construct additional drones. You can move your camera using the WASD keys and zoom in/out with Q and E. Somewhere to the left of your mothership you will find your orange opponent, who is already busy plotting your downfall.
Ok, so let's actually tell our mothership to do something.
The first thing we want is to build a new drone that will scout the map, harvest minerals and bring them back to the mothership.
To do so, we override the DroneController.onSpawn
method inside the Mothership
class.
This method is called automatically by the game engine when our drone first spawns.
Inside onSpawn
, we call buildDrone(controller, droneSpec)
. This method take two arguments:
controller
is another DroneController that will govern the behaviour of our new Drone.droneSpec
is a DroneSpec object which specifies what modules our new drone will have. (in this case, we want two storage modules which will allow us to harvest and transport resources)
In Scala, you can use a simpler variant of buildDrone that allows you to directly specify with modules using named parameters instead of creating a DroneSpec object. Add the following code to your Mothership class:
override def onSpawn(): Unit = buildDrone(new Harvester, storageModules = 2)
@Override public void onSpawn() {
DroneSpec harvesterSpec = new DroneSpec().withStorageModules(2);
buildDrone(new Harvester(), harvesterSpec);
}
Of course we still need to implement the Harvester
, so create a new file with the following contents:
import cwinter.codecraft.core.api._
import cwinter.codecraft.util.maths.Vector2
import scala.util.Random
class Harvester extends DroneController {
override def onTick(): Unit = {
if (!isMoving) {
val randomDirection = Vector2(2 * math.Pi * Random.nextDouble())
val targetPosition = position + 500 * randomDirection
moveTo(targetPosition)
}
}
}
import cwinter.codecraft.core.api.*;
import cwinter.codecraft.util.maths.Vector2;
import java.util.Random;
class Harvester extends JDroneController {
static Random rng = new Random();
@Override public void onTick() {
if (!isMoving()) {
Vector2 randomDirection = new Vector2(2 * Math.PI * rng.nextDouble());
Vector2 targetPosition = position.plus(randomDirection.times(500));
moveTo(targetPosition);
}
}
}
This time, we override the onTick
method which is called on every timestep.
First, we test whether the drone is currently moving using the isMoving property.
If this is not the case, we give the drone a command to move into a new random direction for 100 timesteps using the moveInDirection
method.
You should now run the program again and verify that your mothership constructs a new drone which moves randomly across the map.
We still want to harvest resources and return them to the mothership.
For this, we override the onMineralEntersVision
method which is called whenever a mineral crystal enters the sight radius of our drone.
When this happens, we want to stop scouting and move towards the mineral crystal.
Once we have arrived, the onArrivesAtMineral
method is called, where we give orders to harvest the mineral.
We also modify our code in the onTick
method to send the drone back to the mothership when it's storage is full.
Once the drone arrives there, the onArrivesAtDrone
method will be called where we give orders to deposit the mineral crystals.
The HarvesterController class should now look like this:
class Harvester(mothership: DroneController) extends DroneController {
override def onTick(): Unit = {
if (!isMoving && !isHarvesting) {
if (availableStorage == 0) moveTo(mothership)
else {
val randomDirection = Vector2(2 * math.Pi * Random.nextDouble())
val targetPosition = position + 500 * randomDirection
moveTo(targetPosition)
}
}
}
override def onMineralEntersVision(mineral: MineralCrystal) =
if (availableStorage > 0) moveTo(mineral)
override def onArrivesAtMineral(mineral: MineralCrystal) = harvest(mineral)
override def onArrivesAtDrone(drone: Drone) = giveResourcesTo(drone)
}
class Harvester extends JDroneController {
static Random rng = new Random();
private JDroneController mothership;
public Harvester(JDroneController mothership) {
this.mothership = mothership;
}
@Override public void onTick() {
if (!isMoving() && !isHarvesting()) {
if (availableStorage() == 0) moveTo(mothership);
else {
Vector2 randomDirection = Vector2(2 * Math.PI * rng.nextDouble());
Vector2 targetPosition = position.plus(randomDirection.times(500));
moveTo(targetPosition);
}
}
}
@Override public void onMineralEntersVision(MineralCrystal mineral) {
if (availableStorage() > 0) moveTo(mineral);
}
@Override public void onArrivesAtMineral(MineralCrystal mineral) {
harvest(mineral);
}
@Override public void onArrivesAtDrone(Drone drone) {
giveResourcesTo(drone);
}
}
We also need to make a small change to the Mothership
controller.
The HarvesterController
now expects a reference to the mothership as argument, so it knows where to return the minerals.
We are also going to move the construction code into the onTick
method so that we keep building new drones.
The Mothership
class now looks like this:
class Mothership extends DroneController {
override def onTick(): Unit =
if (!isConstructing) buildDrone(new Harvester(this), storageModules = 2)
}
import cwinter.codecraft.core.api.*;
class Mothership extends JDroneController {
static final DroneSpec HARVESTER_SPEC = new DroneSpec().withStorageModules(2);
@Override public void onTick() {
if (!isConstructing()) {
buildDrone(new Harvester(this), HARVESTER_SPEC);
}
}
}
If you run your program again now, you should see a growing armada of harvesters collecting all the mineral crystals.
Now that you have laid the economic foundations for your drone empire, it is time to start thinking about how to beat your opponent.
By now you probably got the hang of how CodeCraft works, so I will just give you a high level overview of how to implement your army.
After you have built some harvesters, you will want to start production on a different type of drone.
Instead of storageModules
, it should be equipped with one or more missileBatteries
and maybe even shieldGenerators
.
Another controller will be required as well, and you will find the following methods useful:
- The
DroneController
methoddronesInSight
returns aSet
of allDrone
s which can be seen by this drone controller - The
Drone
class has a methodisEnemy
which tells you whether that drone is an enemy - The
DroneController
methodisInMissileRange(target: Drone)
can be used to check whether some drone is within the range of your missiles - The
DroneController
methodfireMissilesAt(target: Drone)
will fire all your missiles at the dronetarget
- If you are using Java method parentheses aren't optional, so you will need to write e.g.
dronesInSight()
andisEnemy()
If you don't quite manage to get all of this to work, you can check out the scala-solution and java-solution directories in this repo, which contain a full implementation of everything described in this tutorial. If you want to go even further, check out the next section which gives an overview of all the other parts of the API which haven't been covered yet. If you write an AI that uses a different strategy than those in the current levels (or which beats all levels), please send me a link to your code at codecraft@gmail.com and I'll include in the next release.
Hopefully this tutorial has succeded in giving you a good understanding of what CodeCraft is about and how to use it. If you want to continue building out your AI, you can get answers to any questions you have from the comprehensive documentation or fellow CodeCraft users. In addtiion to that, this section gives a quick overview of the most useful parts of the API.
To configure and start the game, you use TheGameMaster
object.
TheGameMaster
has methods runLevel1
, runLevel2
, ..., runLevel7
which take as an argument the DroneController
for your mothership and start the corresponding level.
There is also a runGame
method which allows you to start a game with two custom DroneController
s.
The game automatically records replays of all games in the folder ~/.codecraft/replays
.
You can run the last recorded replay using runLastReplay
and run the replay with a specific filename using runReplay
.
You can start a multiplayer game using TheGameMaster.prepareMultiplayerGame
. This returns a Future[DroneSimulator]
which completes as soon as another player connects to the same server:
import cwinter.codecraft.core.api._
import scala.concurrent.Await
import scala.concurrent.duration._
object Main {
def main(args: Array[String]): Unit = {
val game = Await.result(
TheGameMaster.prepareMultiplayerGame("SERVER ADDRESS", new Mothership), 10.minutes)
TheGameMaster.run(game)
}
}
You can run a server by calling cwinter.codecraft.core.multiplayer.Server.start()
.
Almost all interactions with the game world goes through this class.
If you are using Java, you should use the JDroneController
class instead, which is almost identical but returns Java collections rather than their Scala counterparts.
DroneController
has three different kinds of methods:
- Event handlers such as
onSpawn
andonDroneEntersVision
. These are automatically called by the game on specific events and you can override them to respond to these events. - Commands such as
moveTo
andbuildDrone
. You can call these methods to make your drones perform various actions. - Properties such as
position
andhitpoints
which allow you to query the current state of the drone.
You can find a complete description of all of them in the API reference.
In some methods (e.g. onDroneEntersVision
) you are given a Drone
object.
Since this could reference an enemy drone, it only exposes a subset of the properties and none of the event and command methods.
Vector2
is an immutable 2D vector and used throughout CodeCraft.
It defines various methods and operators to perform e.g. vector addition, scalar multiplication and compute it's length.
Details can be found in the API reference.
You can display a string at any position in the game world by using the DroneController.showText(text: String, position: Vector2)
method.
This is only valid for one timestep, so you if you will need to call this method on every timestep on which you want the text to be displayed.
E.g. if you wanted your drones to display their position, you could use this code:
override def onTick(): Unit = {
showText(position.toString, position)
}