Flattener
Origins
The flattener was developed by the RoboWiki wave-surfing community. Patrick Cupka (Voidious) described enabling it only above a certain enemy hit percentage in his bot Diamond, and bots such as YersiniaPestis use several activation levels instead of an on/off switch.
A wave surfer learns from being hit. Every bullet that lands raises the danger at that GuessFactor, and the surfer steers elsewhere next time. That works well against a gun that aims where bullets used to connect. But some guns, including many anti-surfer guns, learn from where the bot went, whether or not a bullet was there. Voidious noted that his anti-surfer gun decays its data by visits, because it knows nothing about its own hits. To such a gun, the surfer's favorite escape spots are a target.
Flat means unpredictable
RoboWiki defines a movement profile as a graph of how much time a bot spent at each GuessFactor. A perfectly flat profile is the classic goal of random movement, because it gives a learning gun no angle worth preferring.
Plain surfing does not aim for flatness. It aims for low danger, and the safest spots against one gun can become well-worn paths. A flattener is RoboWiki's name for any technique where a bot tracks its own movement, the way an enemy gun would, and avoids moving the same way too often.
Schematic profiles: plain surfing leaves peaks a visit-counting gun can find, a flattener spreads them.
How a flattener works
A flattener needs no new machinery. It uses the same GuessFactor statistics the surfer already keeps for enemy hits, but it records the bot's position on every wave that reaches it, hit or not. The surfer then adds that visit danger to the hit danger when it picks where to go.
The class below keeps both tables, normalizes each by its own count so neither swamps the other, and spreads every entry over nearby bins with BasicSurfer's smoothing. The bin values come from the same GuessFactor conversion the surfer already uses.
public final class FlatteningDanger {
private static final int BINS = 47;
private final double[] hitStats = new double[BINS];
private final double[] visitStats = new double[BINS];
private final double startRate;
private final double fullRate;
private int waves;
private int hits;
public FlatteningDanger(double startRate, double fullRate) {
this.startRate = startRate;
this.fullRate = fullRate;
}
public void onWaveReached(int bin, boolean hitMe) {
waves++;
smoothAdd(visitStats, bin);
if (hitMe) {
hits++;
smoothAdd(hitStats, bin);
}
}
public double danger(int bin) {
double hitDanger = hitStats[bin] / Math.max(1, hits);
double visitDanger = visitStats[bin] / Math.max(1, waves);
return hitDanger + flattenerWeight() * visitDanger;
}
public double flattenerWeight() {
double hitRate = waves == 0 ? 0 : (double) hits / waves;
return Math.max(0, Math.min(1, (hitRate - startRate) / (fullRate - startRate)));
}
private static void smoothAdd(double[] stats, int bin) {
for (int i = 0; i < BINS; i++) {
stats[i] += 1.0 / ((i - bin) * (i - bin) + 1);
}
}
}BINS = 47
class FlatteningDanger:
def __init__(self, start_rate: float, full_rate: float) -> None:
self.start_rate = start_rate
self.full_rate = full_rate
self.hit_stats = [0.0] * BINS
self.visit_stats = [0.0] * BINS
self.waves = 0
self.hits = 0
def on_wave_reached(self, bin_index: int, hit_me: bool) -> None:
self.waves += 1
smooth_add(self.visit_stats, bin_index)
if hit_me:
self.hits += 1
smooth_add(self.hit_stats, bin_index)
def danger(self, bin_index: int) -> float:
hit_danger = self.hit_stats[bin_index] / max(1, self.hits)
visit_danger = self.visit_stats[bin_index] / max(1, self.waves)
return hit_danger + self.flattener_weight() * visit_danger
def flattener_weight(self) -> float:
hit_rate = self.hits / self.waves if self.waves else 0.0
return max(0.0, min(1.0, (hit_rate - self.start_rate) / (self.full_rate - self.start_rate)))
def smooth_add(stats: list[float], bin_index: int) -> None:
for i in range(BINS):
stats[i] += 1 / ((i - bin_index) ** 2 + 1)public final class FlatteningDanger {
private static final int BINS = 47;
private final double[] hitStats = new double[BINS];
private final double[] visitStats = new double[BINS];
private final double startRate;
private final double fullRate;
private int waves;
private int hits;
public FlatteningDanger(double startRate, double fullRate) {
this.startRate = startRate;
this.fullRate = fullRate;
}
public void onWaveReached(int bin, boolean hitMe) {
waves++;
smoothAdd(visitStats, bin);
if (hitMe) {
hits++;
smoothAdd(hitStats, bin);
}
}
public double danger(int bin) {
double hitDanger = hitStats[bin] / Math.max(1, hits);
double visitDanger = visitStats[bin] / Math.max(1, waves);
return hitDanger + flattenerWeight() * visitDanger;
}
public double flattenerWeight() {
double hitRate = waves == 0 ? 0 : (double) hits / waves;
return Math.max(0, Math.min(1, (hitRate - startRate) / (fullRate - startRate)));
}
private static void smoothAdd(double[] stats, int bin) {
for (int i = 0; i < BINS; i++) {
stats[i] += 1.0 / ((i - bin) * (i - bin) + 1);
}
}
}using System;
public sealed class FlatteningDanger
{
private const int Bins = 47;
private readonly double[] hitStats = new double[Bins];
private readonly double[] visitStats = new double[Bins];
private readonly double startRate;
private readonly double fullRate;
private int waves;
private int hits;
public FlatteningDanger(double startRate, double fullRate)
{
this.startRate = startRate;
this.fullRate = fullRate;
}
public void OnWaveReached(int bin, bool hitMe)
{
waves++;
SmoothAdd(visitStats, bin);
if (hitMe)
{
hits++;
SmoothAdd(hitStats, bin);
}
}
public double Danger(int bin)
{
double hitDanger = hitStats[bin] / Math.Max(1, hits);
double visitDanger = visitStats[bin] / Math.Max(1, waves);
return hitDanger + FlattenerWeight() * visitDanger;
}
public double FlattenerWeight()
{
double hitRate = waves == 0 ? 0 : (double)hits / waves;
return Math.Clamp((hitRate - startRate) / (fullRate - startRate), 0, 1);
}
private static void SmoothAdd(double[] stats, int bin)
{
for (int i = 0; i < Bins; i++)
{
stats[i] += 1.0 / ((i - bin) * (i - bin) + 1);
}
}
}class FlatteningDanger {
private static readonly bins = 47;
private readonly hitStats = new Array<number>(FlatteningDanger.bins).fill(0);
private readonly visitStats = new Array<number>(FlatteningDanger.bins).fill(0);
private waves = 0;
private hits = 0;
constructor(private readonly startRate: number, private readonly fullRate: number) {}
onWaveReached(bin: number, hitMe: boolean) {
this.waves += 1;
smoothAdd(this.visitStats, bin);
if (hitMe) {
this.hits += 1;
smoothAdd(this.hitStats, bin);
}
}
danger(bin: number) {
const hitDanger = this.hitStats[bin] / Math.max(1, this.hits);
const visitDanger = this.visitStats[bin] / Math.max(1, this.waves);
return hitDanger + this.flattenerWeight() * visitDanger;
}
flattenerWeight() {
const hitRate = this.waves === 0 ? 0 : this.hits / this.waves;
return Math.max(0, Math.min(1, (hitRate - this.startRate) / (this.fullRate - this.startRate)));
}
}
function smoothAdd(stats: number[], bin: number) {
for (let i = 0; i < stats.length; i += 1) {
stats[i] += 1 / ((i - bin) * (i - bin) + 1);
}
}Call onWaveReached when a tracked enemy wave passes the bot, then use danger wherever the surfer scored a spot before, for example in the predictor from Wave Surfing Implementations. A variant discussed on the talk page, the tick flattener, logs a visit on every turn instead of once per wave, which collects far more samples per round.
When to switch it on
Flattening is not free. Every unit of visit danger pulls the surfer away from spots that its hit data calls safe, so against a gun that rarely hits, a flattener mostly adds noise. Diamond, for example, enables its flattener only above a certain enemy hit percentage. YersiniaPestis uses a non-binary flattener with several activation levels.
flattenerWeight follows that gradual idea. Below startRate the flattener is off, above fullRate it counts fully, and in between it blends. RoboWiki does not publish the thresholds that Diamond uses, so treat both values as tuning parameters and test them against both simple and learning guns.
The same rules apply in classic Robocode and Tank Royale, since only GuessFactor data is involved.
An arms race, not an ending
A flattener answers one kind of gun, and the other side adapts too. On the talk page, Straw points out that a gun tuned for surfers, with fast decay that fires where they have been, differs from a gun tuned for flattened movement. MN suggests firing more often at the angles a surfer visits least. Each counter invites another, so a flattened surfer is never finished. The gun side of this contest continues in Anti-Surfer Targeting.
Further Reading
- Flattener - RoboWiki (classic Robocode)
- Talk:Flattener - RoboWiki (classic Robocode)
- Movement Profile - RoboWiki (classic Robocode)
- Wave Surfing Tutorial - RoboWiki (classic Robocode)
- Anti-Surfer Targeting - RoboWiki (classic Robocode)