Robo Home | CassiusClay | Changes | Preferences | AllPages

/Butterfly by PEZ - The CassiusClay (pluggable) movement

Butterfly is all about WaveSurfing. It's the result of letting the Pugilist movement grow out of its MiniBot constraints. The design is pretty simple:

  1. Every scan create an EnemyWave
    1. If the enemy fired the tick before then mark the wave as "surfable"
    2. If a bullet hits my bot then
      1. Find the wave that is in the passing
      2. Find out what GuessFactor we were hit on
      3. Update the stat buffers with this knowledge
  2. Every scan:
    1. check where (what guess factor) we would impact with the closest surfable wave were we to:
      1. move fully forward
      2. move fully reverse
      3. hit the breaks
    2. For each wave in the air
      1. Examine how often we've been hit in each of these destination options compared to all other guess factors
      2. Weigh together the results of all surfable waves with the closest one being the most important
    3. Go for the option where we've been hit the least (or where it seems the least dangerous to move)

Simple enough, huh? One of those steps is a bit complicated though - step 2.1. It involves predicting our own movement some ticks in the future. This can be done in many ways. Pugilist does it in a pretty sloppy manner. CassiusClay is a bit more precise. In fact as of version (g for gamma, my first test version) not only considers how fast CC can accelerate/deccelerate. It also tries to take into account the maximum turn angle it can do at a given velocity. Here's the scheme for the predictor:



  1. Grab the current heading
  2. Grab the current velocity (relative to the desired orbit direction)
  3. Do
    1. Calculate a WallSmoothed position in the desired orbit direction
    2. Calculate absolute bearing to this position
    3. Adjust the velocity according to Robocode physics (aiming for the target velocity)
    4. Calculate how much we would need to turn the bot to get the right heading
    5. Adjust our virtual heading while considering BackAsFront and the maximum turn rate at the current virtual velocity
    6. Calculate the position of the bot 1 tick into the future using the new virtual heading and velocity
    7. Advance the wave 1 tick into the future
  4. Until the wave impacts with the predicted bot position

Here's the implementing code (RWPCL):

    Move waveImpactLocation(MovementWave closest, double direction, double maxVelocity) {
	double currentDirection = robotOrbitDirection(closest.gunBearing(robotLocation));
	double v = Math.abs(robot.getVelocity()) * PUtils.sign(direction);
	double h = robot.getHeadingRadians();
	Point2D orbitCenter = orbitCenter(closest);
	Point2D impactLocation = new Point2D.Double(robot.getX(), robot.getY());
	Move smoothed;
	int time = 0;
	do {
	    smoothed = wallSmoothedDestination(impactLocation, orbitCenter, currentDirection * direction);
	    double wantedHeading = PUtils.absoluteBearing(impactLocation, smoothed.location);
	    h += PUtils.backAsFrontDirection(wantedHeading, h) < 0 ? Math.PI : 0.0;
	    if (v < maxVelocity) {
		v = Math.min(maxVelocity, v + (v < 0 ? 2 : 1));
	    else {
		v = Math.max(maxVelocity, v - 2);
	    double maxTurn = Math.toRadians(MAX_TURN_RATE - 0.75 * Math.abs(v));
	    h += PUtils.minMax(PUtils.backAsFrontTurn(wantedHeading, h), -maxTurn, maxTurn);
	    impactLocation = PUtils.project(impactLocation, h, v);
	} while (closest.distanceFromTarget(impactLocation, time++) > 18);
	return new Move(impactLocation, smoothed.smoothing, smoothed.wantedEvasion, smoothed.oldDistance, impactLocation.distance(enemyLocation));

The BackAsFront functions there looks like so:

    public static double backAsFrontTurn(double newHeading, double oldHeading) {
	return Math.tan(newHeading - oldHeading);

    public static double backAsFrontDirection(double newHeading, double oldHeading) {
	return sign(Math.cos(newHeading - oldHeading));

It's the same functions I use to move my bot around Jamougha style:

	double newHeading = PUtils.absoluteBearing(robotLocation, destination);
	double oldHeading = robot.getHeadingRadians();
	robot.setAhead(PUtils.backAsFrontDirection(newHeading, oldHeading) * 50);
	robot.setTurnRightRadians(PUtils.backAsFrontTurn(newHeading, oldHeading));

-- PEZ


I just noticed this page! Cool, since this is the best explanation of WaveSurfing I've seen yet. I think I understand the design (except for how you do WallSmoothing). From reading the sourcecode I have no clue what could cause the problem you mention, but I do have an idea that may give you visual feedback on where the problem might occur: maybe you could modify CC so that it only tries one out of your three movement options. For instance 'move fully forward' (I assume CC will automatically choose that one option for real). Then in your waveImpactLocation?() function store all predicted positions in an array. Make sure this array is accessible to the GL version of CC. Then plot these points using CCGL and watch if the position of CC each tick corresponds with the predicted positions. Test each of the three movement options separately. I once did something similar with testing my PatternMatching gun. I plotted the enemy's predicted path in this way, and tested using bots with simple movement. It was a huge help in ironing out the bugs. Maybe it helps in this case as well. --Vic

Thanks! I did something like that actually, though not in GL. Maybe it wasn't clear, but I have verified now that the above code does work for its purpose. So there's no problem. But I'll let the page be published. Maybe someone can learn something from it. Since you might still be wondering, here's the wall smoothing function:

    static Move wallSmoothedDestination(Point2D location, Point2D orbitLocation, double direction) {
	Point2D destination = new Point2D.Double();
	double distance = enemyLocation.distance(location);
	double evasion = evasion(distance);
	double blindStick = enemyIsRammer() ? PUtils.minMax(enemyDistance / 2, 60, 170) : 150;
	double smoothing = 0;
	while (!fieldRectangle.contains(destination = PUtils.project(location,
			PUtils.absoluteBearing(location, orbitLocation) - direction * ((evasion - smoothing / 100) * Math.PI / 2), blindStick)) && smoothing < MAX_WALL_SMOOTH_TRIES) {
	return new Move(destination, smoothing, evasion, distance, destination.distance(enemyLocation));
Yes, much the same as RandomMovementBot is using.

-- PEZ

Ok, thanx. Surprisingly simple solution! --Vic

Maybe so. But I'm very proud of it! =) It started the whole Tityus - Raiko race and was a major key to create two really competetive micros (RaikoMicro and Aristocles). -- PEZ

Don't get me wrong, that was a compliment! Simple==good (when it works:) --Vic

I didn't get you wrong. See that smiley? I think most of us like simplicity in key parts of our bots. It gives room for complexity elsewhere if nothing else. -- PEZ

Why are you firing enemy waves every tick? Isn't it enough to just fire 'surfable' waves? --Vic

For the purposes of the above described scheme, yes. But part of the story is not told. =) I have a flattening "under current" in my surfing. And that surfing uses visit counts from those every-tick waves. Furthermore, many of my methods and functions need a wave to work with and since i don't always have a fresh surfable wave at hand i hand them the latest created wave, regardless if it's surfabe or not. -- PEZ

Great explanation! May be I'w even try a bot with WaveSurfing. BTW, to calculate your exact future position, you can use FuturePosition. -- Albert

Thanks! I was hoping it would help make WaveSurfing a bit less voodoo-ish. Funny that the author of FuturePosition would be someone finding the explanation useful. =) Note that this is just one way to go about wave surfing. Axe is doing something different. Which he eventualy will explain on the SilverSurfer/WaveSuffering page. -- PEZ

I was studying the code to try to understand why it was so good and I found this (lines 121-125)
if (velocityIndex != lastVelocityIndex) {
	timeSinceVChange = 0;
	wave.accelIndex = velocityIndex < lastVelocityIndex ? 1 : 2;
wave.accelIndex = 0;
shouldn't it be
wave.accelIndex = 0;
if (velocityIndex != lastVelocityIndex) {
	timeSinceVChange = 0;
	wave.accelIndex = velocityIndex < lastVelocityIndex ? 1 : 2;

Florent, it looks like you must be right about that, so I figured I'd test it in the CurveFlatteningChallenge both ways. The score was actually higher with it the way it is, so maybe he did that to just disable the accelIndex? It was 96.52% vs 97.32%. A few matches vs Ascendant all gave CC a higher score percentage than the 40.8% he has now in the RR, though, so maybe it really is a mistake! I'm tempted to make a new bot that is CC with that one thing changed and enter it in the RoboRumble to see what happens... ;) -- Voidious

Hey PEZ, you probably never saw this comment, but now that you're checking the wiki again... This certainly seems like it could possibly be a bug in CassiusClay's movement, unless you intended to turn off the accel index. -- Voidious

Wow, that sure looks stupid, doesn't it? It's hard to remember, but I know I wouldn't switch off accelIndex like that. I checked the code now and I saw that the accelIndex isn't used in the segmentation. I think that the Florent-bug made accelIndex useless and thus I probably tuned it away. Now, wouldn't it be a bit interesting to tune CC for using it again now that Florent showed how to enable it? This is prolly extremely belated Florent, but HUGE thanks! And thanks to Voidious for picking it up and for reminding me now that I am paying attention again.

The changes in performance you saw there, V, must have been chance in play. That's how inexact the indexes are I guess.

-- PEZ

I was checking my own math, comparing with yours when I found that:

approachVelocity = velocity * -Math.cos(robot.getHeadingRadians() - (enemyAbsoluteBearing + 180));
That's line 114, I think adding degrees (180) and radians is not a good idea. -- Florent

Wow! And thanks! Even if belated, voidious mentioned this bug report to me yesterday. I'll release a bugfixed version in a jiffy. -- PEZ

Seems I gained 2 points with that bug fix. It might or migt not be a significant change, but at least I think it can be said that fixing this bug didn't make things worse. =) And now that this bug is fixed I might be able to tweak the weights of the segmentation to squeeze out some more points. -- PEZ

Finally somebody came up with the solution

Robo Home | CassiusClay | Changes | Preferences | AllPages
Edit text of this page | View other revisions
Last edited November 9, 2006 12:36 EST by Jeff (diff)