robocode-dev / tank-royale

Git repository for Robocode Tank Royale
Apache License 2.0
122 stars 23 forks source link

Questions about seemingly inconsistent behavior #26

Closed beamer159 closed 2 years ago

beamer159 commented 2 years ago

I want to make a bot that moves in a circle. I coded this in multiple ways that I assumed would behave the same. However, they each have different behaviors. Some of these different behaviors may be intentional, I am just curious about them.

1.

setTurnRight(Double.POSITIVE_INFINITY);
forward(Double.POSITIVE_INFINITY);

This behaves how I would expect. The bot moves in a circle. If it hits an obstacle, it continues attempting to move in a circle. All is well and good

2.

setForward(Double.POSITIVE_INFINITY);
turnRight(Double.POSITIVE_INFINITY);

This behaves similar to 1; the bot moves in a circle. However, if it hits an obstacle, it stops moving forward and spins in place. It seems like forward maintains distance remaining when the bot runs into something, but setForward does not

3.

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);

The bot does not move at all. I expected this because I do not call go, and the documentation says that go must be called every turn if using these functions. Although this seems obvious, I thought there might be a chance that go is called automatically after the run function completes.

4.

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);
go();

The bot moves in a tiny circle. Due to my misunderstanding of go, I was expecting this to either move for a single turn and then stop, or move similar to 1 and 2. However, the forward speed is much smaller than these previous cases. Additionally, it seems like bumping into obstacles does not eliminate its forward movement like 2, even though setForward is used here as well.

I should also mention when I was testing this case, I threw the following exception once at the beginning of a run. This exception seems to occur rarely for some reason, and I think it could have happened during any of these cases.

Exception in thread "Thread-28" java.lang.IllegalArgumentException: -Infinity is not a valid double value as per JSON specification. To override this behavior, use GsonBuilder.serializeSpecialFloatingPointValues() method.

5.

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);
while (true) {
    go();
}

The bot spins in place. Also, when I choose to restart the battle, the battle does not begin. To retry, I have to close the bot process and start it anew. I actually expected this to be the correct way to code a circle bot. The documentation for go says that it must be called every turn, so putting it in a while loop seemed the best way to ensure this. I thought if go is not called, actions would not be sent to the server, hence why I thought case 4 might have the bot only move for a single turn. Currently though, it appears that putting go in a loop with nothing else causes the most problems.

flemming-n-larsen commented 2 years ago

Before I get started answering all the questions for the 5 scenarios, I will provide some info about how Tank Royale works regarding the Bot APIs. The Bot APIs have been created to make life easier as a bot developer, as you could alternatively send and receive messages over websockets in a more raw way using the schema/protocol. But of ocurse the Bot APIs should be the obvious choice for most developers. 🙂

The Run method

The run() method should be seen the same way an ordinary main() method, except that main() is used for startup code (called only once by startup script), and run() is being called for each round by the game.

Note: As soon as you leave the run() method, your bots control over execution is done like a normal program being run inside a main() method. And the game is stopping event processing in the background, even though it sometimes fails, if the run() method goes not terminate. Hence, running code in an infinite loop as you do in scenario 5 is really bad practise, as there is no way to stop the thread when it runs forever.

Hence, avoid infinite loops and use e.g. while (isRunning()) // do stuff as the game will be able to stop the loop.

The Go method

All that go() does it sending an intent with target speed, turning rates etc. to the server. It is an intent as it is a request, not a command that must be obeyed. If e.g. the bot sets the target speed to e.g. 1000, the game will truncate it to 8.

If for some reason the intent is not sent to or received by the server, the server will consider this a skipped turn, and the bot will have no update in movement etc. for that turn, and the game will continue to use the previous values being sent, i.e. the last received instructions for the individual turn rate, target speed etc. A new intent basically overwrittes the previous set values.

Answers to the 5 scenario

With the above in mind, we are ready to examine the scenarios.

Scenario 1

setTurnRight(Double.POSITIVE_INFINITY);
forward(Double.POSITIVE_INFINITY);

Is basically the same as:

setTurnRight(Double.POSITIVE_INFINITY);
setForward(Double.POSITIVE_INFINITY);
go()

forward() is a blocking call that calls setForward() + go() one to many times depending on the input value and distance remaining.

Scenario 2

setForward(Double.POSITIVE_INFINITY);
turnRight(Double.POSITIVE_INFINITY);

Is basically the same as:

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);
go()

Scenario 3

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);

Obviously, this does nothing in itself, as go() is not being called, and hence the intent is not being sent to the server.

Scenario 4

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);
go();

The bot should move in a circle. I guess this is a bug, but in a special scenario where it is the very first round and turn. But nevertheless, a bug.

Scenario 5

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);
while (true) {
    go();
}

The infinite loop is BAD practice here, and the cause of your trouble. Robocode cannot stop your thread, and will wait for it to stop before starting a new one (to avoid multiple threads to do different things on the bot causing serious raise conditions that are hard to cope with).

Try this instead:

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);
while (isRunning()) {
    go();
}

JSON floating point

Thanks for the tip about:

Exception in thread "Thread-28" java.lang.IllegalArgumentException: -Infinity is not a valid double value as per JSON specification. To override this behavior, use GsonBuilder.serializeSpecialFloatingPointValues() method.

I will fix this too of course. 🙂

flemming-n-larsen commented 2 years ago

Regarding scenario 4, I am not sure this is a bug. It seems that these two behave the same, which is consistent:

setForward(Double.POSITIVE_INFINITY);
setTurnRight(Double.POSITIVE_INFINITY);
go();

Compared to:

setTurnRight(Double.POSITIVE_INFINITY);
setForward(Double.POSITIVE_INFINITY);
go();

Behaves the same way.

beamer159 commented 2 years ago

Should scenarios 1, 2, and 4 all behave the same?

flemming-n-larsen commented 2 years ago

@beamer159

Scenario 1, 2, 4 are not entirely the same. In the end it depends on the blocking call, which will wait (block) until its condition it met, before continuing to process the next instruction, if it exists. And since they are waiting for an infitite value to be reached, this will never happen, and they will wait forever - at least for scenarios 1 and 2.

With scenario 4, we only call a single go(), which sends the intent to the server (once), and then immidiately moves to the next instruction, if any. But it will not hang until the infinite value is reached. 🙂

The behavior is described in the code with the BotInternals class. Here it is for the Java API: https://github.com/robocode-dev/tank-royale/blob/master/bot-api/java/src/main/java/dev/robocode/tankroyale/botapi/internal/BotInternals.java

Have a look at forward, turnLeft, turnGunLeft, turnRadarleft. . 🙂

beamer159 commented 2 years ago

You may already know this, but the following code moves the bot quite slowly.

setForward(Double.POSITIVE_INFINITY);
go();

This works better though.

setForward(Double.POSITIVE_INFINITY);
while (isRunning()) {
    go();
}
flemming-n-larsen commented 2 years ago

@beamer159

I believe that setForward(Double.POSITIVE_INFINITY) should set the target speed to the max. speed = 8. Hence the bot should accelerate with 1 unit/turn when calling setForward with an infite value.

A fix is available with the next version.

flemming-n-larsen commented 2 years ago

@beamer159

Some of the issues observed with #20 might also might some of the bugs seen in this one with release 0.13.3.

And note that the first turn in the first round migth skip the first go() instruction. So if run() is only calling a single go() before it exists, then the bot might not move. So make sure to have a couple of go's if you are not using a loop.

flemming-n-larsen commented 2 years ago

@beamer159

I believe that setForward(Double.POSITIVE_INFINITY) should set the target speed to the max. speed = 8. Hence the bot should accelerate with 1 unit/turn when calling setForward with an infite value.

A fix is available with the next version.

You may already know this, but the following code moves the bot quite slowly.

setForward(Double.POSITIVE_INFINITY);
go();

This works better though.

setForward(Double.POSITIVE_INFINITY);
while (isRunning()) {
    go();
}

This is also fixed with release 0.13.3

flemming-n-larsen commented 2 years ago

I fixed the issues we have discussed. 😊