Midboss/Score-Estimation based Firepower Selection

From Robowiki
< Midboss
Revision as of 04:31, 24 June 2021 by Rednaxela (talk | contribs) (Whoops, posted a bad version of BulletPowerSelector that was based on de-compiling a hack-y version once upon a time. Found the real code.)
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Once upon a time in 2010-2011 some versions of Midboss I was employing a very intelligent firepower selection algorithm based on score prediction.

The current version of Midboss in the rumble is not using the most recent version of this code than is pasted here, and I'm not quite sure how different the old version is off hand. In any case, I consider it rather ugly at this point, and it's quite slow, but some folks might find it interesting:

package ags.muse.gun;

import robocode.Rules;

public class BulletPowerSelector {
    HitrateEstimator selfEstimator;
    HitrateEstimator enemyEstimator;

    public BulletPowerSelector() {
        this.selfEstimator = new HitrateEstimator();
    }

    public void setEnemyEstimator(HitrateEstimator enemyEstimator) {
        this.enemyEstimator = enemyEstimator;
    }

    public void waveEnd(SWave wave) {
        selfEstimator.waveEnd(wave.getHitRange().intersects(wave.getAimGF()), wave.getInitDistance(), wave.getSpeed());
    }

    public double getBestPower(double distance, double myEnergy, double myDamageTaken, double enemyEnergy, double enemyDamageTaken, double enemyFirepower, int enemyCoolticks) {
        if (enemyEnergy < 0.1)
            return 0.1;
        if (distance < 50)
            return 3.0;
        //if (selfEstimator.getWaveCount()<20 || enemyEstimator.getWaveCount()<20)
        //    return Math.min(1.9, damageToBulletPower(enemyEnergy));

        double bestPower=Double.NaN;
        double bestScore=Double.NEGATIVE_INFINITY;
        double pNeeded = Math.max(0.1, damageToBulletPower(enemyEnergy));
        //java.text.DecimalFormat twoPlaces = new java.text.DecimalFormat("00.00");
        //Estimation be = null;
        //Estimation me = null;
        for (int i = 2; i <= 60; i++) {
            double p = i/20.0;
            if (p>3) p = 3;
            Estimation e = new Estimation(distance, p, enemyFirepower, enemyCoolticks);
            double score = e.estimateScore(myEnergy, myDamageTaken, enemyEnergy, enemyDamageTaken);
            //System.out.print(twoPlaces.format(score)+" \t");
            //me = e;
            if (score > bestScore) {
                bestScore = score;
                bestPower = p;
                //be = e;
            }
        }
        /*if (pNeeded < bestPower) {
            System.out.print("Best Power: "+bestPower);
            System.out.println("\tNeeded Power: "+pNeeded);
            System.out.println("Best Score: "+bestScore);
        }*/
        return bestPower;
    }

    private static double damageToBulletPower(double energy) {
        if (energy / 4.0D <= 1.0D) {
            return energy / 4.0D;
        }
        return (energy + 2.0D) / 6.0D;
    }

    public void testEstimator() {
        long ot = System.nanoTime();
        for (int i = 2; i <= 60; i++) {
            double myPower = i / 20.0D;
            double enemyPower = 0.09999999999999432D;
            double myHitrate = 0.8823898603645636D;
            double enemyHitrate = 0.8969126319398489D;
            double myEnergy = 113.19999999999999D;
            double enemyEnergy = 64.200000000000017D;
            double myDamageTaken = 0.8000000000000019D;
            double enemyDamageTaken = 36.0D;

            Estimation e = new Estimation(myPower, myHitrate, enemyPower, enemyHitrate, 0);
            double score = e.estimateScore(myEnergy, myDamageTaken, enemyEnergy, enemyDamageTaken);
            System.out.println(myPower + " Test score: " + score);
        }
        System.out.println(System.nanoTime() - ot);
    }

    public static void main(String[] args) {
        BulletPowerSelector bps = new BulletPowerSelector();
        bps.testEstimator();
    }

    public class Estimation {
        java.util.HashMap<SimConfig, Double> cache = new java.util.HashMap<SimConfig, Double>();
        double myHitrate, myDamage, myPower;
        double enemyHitrate, enemyDamage, enemyPower;
        double myLossRate, myKillRate, enemyLossRate, enemyKillRate;
        double myLossRateSD, myKillRateSD, enemyLossRateSD, enemyKillRateSD;
        int enemyCoolticks;
        int enemyCooldown, myCooldown;
        double distance;
        int chits, cmiss;
        private Estimation(double distance, double myPower, double enemyPower, int enemyCoolticks) {
            this(myPower,  selfEstimator.getHitrate(distance, myPower), enemyPower, enemyEstimator.getHitrate(distance, enemyPower), enemyCoolticks);
            this.distance = distance;
        }
        private Estimation(double myPower, double myHitrate, double enemyPower, double enemyHitrate, int enemyCoolticks) {
            this.enemyPower = enemyPower;
            this.enemyHitrate = Math.max(0.0001, enemyHitrate);
            this.enemyDamage = Rules.getBulletDamage(enemyPower);
            this.enemyCooldown = (int)Math.ceil(Rules.getGunHeat(enemyPower)/0.1);
            this.enemyCoolticks = enemyCoolticks;
            this.myPower = myPower;
            this.myHitrate =  Math.max(0.0001, myHitrate);
            this.myDamage = Rules.getBulletDamage(myPower);
            this.myCooldown = (int)Math.ceil(Rules.getGunHeat(myPower)/0.1);

            double myLossBase = myPower/myCooldown;
            this.myLossRate = myLossBase*(1-3*myHitrate);
            this.myLossRateSD = myLossBase*myLossBase*3*3*(myHitrate*(1-myHitrate));
            double myKillBase = myDamage/myCooldown;
            this.myKillRate =  myKillBase*myHitrate;
            this.myKillRateSD = myKillBase*myKillBase*(myHitrate*(1-myHitrate));
            
            double enemyLossBase = enemyPower / (enemyCooldown+enemyCoolticks);
            this.enemyLossRate = enemyLossBase*(1-3*enemyHitrate);
            this.enemyLossRateSD = enemyLossBase*enemyLossBase*3*3*(enemyHitrate*(1-enemyHitrate));
            double enemyKillBase = enemyDamage/(enemyCooldown+enemyCoolticks);
            this.enemyKillRate = enemyKillBase*enemyHitrate;
            this.enemyKillRateSD = enemyKillBase*enemyKillBase*(enemyHitrate*(1-enemyHitrate));
        }

        private double estimateScore(double myEnergy, double myDamageTaken, double enemyEnergy, double enemyDamageTaken) {
            return estimateScore(0, 1, myEnergy, 0, myDamageTaken, enemyEnergy, Math.max(1, enemyCooldown/2), enemyDamageTaken);
        }

        private double estimateScore(int depth, double prob, double myEnergy, Integer myHeat, double myDamageTaken, double enemyEnergy, Integer enemyHeat, double enemyDamageTaken) {
            if (myHeat == null) myEnergy = 0;
            if (enemyHeat == null) enemyEnergy = 0;

            // If someone is dead, finish the calculation
            if (myEnergy == 0 || enemyEnergy == 0) {
                if (myEnergy == 0 && enemyEnergy == 0) {
                    // Tied
                    double ret = 0.1*enemyDamageTaken - 0.1*myDamageTaken;
                    return ret;
                } else if (enemyEnergy == 0) {
                    // Win
                    double ret = 60 + 0.2*enemyDamageTaken;
                    return ret;
                } else {
                    // Loss
                    double ret = - 60 - 0.2*myDamageTaken;
                    return ret;
                }
            }

            // If depth is high, do a quick estimation to skip forward
            if (prob < 0.002 || depth > 30) {
                double myRate = myLossRate + enemyKillRate;
                double myRateSD = myLossRateSD + enemyKillRateSD;
                double enemyRate = enemyLossRate + myKillRate;
                double enemyRateSD = enemyLossRateSD + myKillRateSD;

                double myRating = myEnergy * enemyRate;
                double myRatingSD = myEnergy * myEnergy * enemyRateSD;
                double enemyRating = enemyEnergy * myRate;
                double enemyRatingSD = enemyEnergy * enemyEnergy * myRateSD;

                double ratingSum = myRating - enemyRating;
                double ratingSumSD = myRatingSD + enemyRatingSD;
                
                double myTimeLeft = (myRate > 0) ? myEnergy / myRate : Double.POSITIVE_INFINITY;
                double enemyTimeLeft = (enemyRate > 0) ? enemyEnergy / enemyRate : Double.POSITIVE_INFINITY;
                double expectedTimeLeft = Math.min(myTimeLeft, enemyTimeLeft);
                if (Double.isInfinite(expectedTimeLeft)) expectedTimeLeft = 0;
                
                // Predict the future state of affairs
                myDamageTaken += expectedTimeLeft*enemyKillRate;
                enemyDamageTaken += expectedTimeLeft*myKillRate;
                double thisBulletScore = expectedTimeLeft*(myKillRate - enemyKillRate);

                double winChance = 1.0 - ags.util.StatMath.phi(0, ratingSum, Math.sqrt(ratingSumSD));

                double h1 = (1.0-winChance) * (-60 - 0.2*myDamageTaken);
                double h2 = winChance * (60 + 0.2*enemyDamageTaken);
                double ret = thisBulletScore + h1 + h2;
                return ret;
            }

            // Check cache for the possibility already being calculated
            SimConfig conf = new SimConfig(myEnergy, myHeat, myDamageTaken, enemyEnergy, enemyHeat, enemyDamageTaken);
            Double cachevalue = cache.get(conf);
            if (cachevalue != null) { return cachevalue; }

            if (enemyHeat == null || myHeat <= enemyHeat) {
                // Self fires
                // Calculate next gunheats/energy
                double hitDamage = Math.min(enemyEnergy, myDamage);

                if (myPower < myEnergy) {
                    myEnergy -= myPower;
                    double hitProb = myHitrate;
                    enemyHeat -= myHeat;
                    myHeat = myCooldown;

                    double hitScore = hitDamage + estimateScore(depth+1, prob*hitProb, myEnergy+3*myPower, myHeat, myDamageTaken, enemyEnergy-hitDamage, enemyHeat, enemyDamageTaken+hitDamage);
                    double missScore = estimateScore(depth+1, prob*(1-hitProb), myEnergy, myHeat, myDamageTaken, enemyEnergy, enemyHeat, enemyDamageTaken);
                    double ret = hitScore*hitProb + missScore*(1-hitProb);
                    cache.put(conf, ret);
                    return ret;
                } else {
                    myHeat = null;
                    double ret = estimateScore(depth+1, prob, myEnergy, myHeat, myDamageTaken, enemyEnergy, enemyHeat, enemyDamageTaken);
                    cache.put(conf, ret);
                    return ret;
                }
            } else if (enemyHeat != null) {
                // Enemy fires
                // Calculate next gunheats/energy
                enemyHeat = enemyCooldown+enemyCoolticks;
                double hitDamage = Math.min(myEnergy, enemyDamage);

                if (enemyPower < enemyEnergy) {
                    enemyEnergy -= enemyPower;
                    double hitProb = enemyHitrate;
                    myHeat -= enemyHeat;
                    enemyHeat = enemyCooldown+enemyCoolticks;

                    double hitScore = hitProb > 0.0D ? -hitDamage + estimateScore(depth+1, prob*hitProb, myEnergy-hitDamage, myHeat, myDamageTaken+hitDamage, enemyEnergy+3*enemyPower, enemyHeat, enemyDamageTaken) : 0.0;
                    double missScore = hitProb < 1.0D ? estimateScore(depth+1, prob*(1-hitProb), myEnergy, myHeat, myDamageTaken, enemyEnergy, enemyHeat, enemyDamageTaken) : 0.0;
                    double ret = hitScore*hitProb + missScore*(1-hitProb);
                    cache.put(conf, ret);
                    return ret;
                } else {
                    enemyHeat = null;
                    double ret = estimateScore(depth+1, prob, myEnergy, myHeat, myDamageTaken, enemyEnergy, enemyHeat, enemyDamageTaken);
                    cache.put(conf, ret);
                    return ret;
                }
            } else {
                return 0;
            }
        }

        private class SimConfig {
            private final double myEnergy, myDamageTaken, enemyEnergy, enemyDamageTaken;
            private final Integer myHeat, enemyHeat;
            private final int hash;
            
            private SimConfig(double myEnergy, Integer myHeat, double myDamageTaken, double enemyEnergy, Integer enemyHeat, double enemyDamageTaken) {
                this.myEnergy = myEnergy;
                this.myHeat = myHeat;
                this.myDamageTaken = myDamageTaken;
                this.enemyEnergy = enemyEnergy;
                this.enemyHeat = enemyHeat;
                this.enemyDamageTaken = enemyDamageTaken;
                long longhash = (
                        Double.doubleToRawLongBits(myEnergy)^
                        Double.doubleToRawLongBits(myDamageTaken)^
                        Double.doubleToRawLongBits(enemyEnergy)^
                        Double.doubleToRawLongBits(enemyDamageTaken)+
                        ((myHeat != null) ? myHeat.hashCode() : -1) +
                        ((enemyHeat != null) ? enemyHeat.hashCode() : -2)
                        );
                this.hash = ((int)longhash) ^ ((int)Long.reverse(longhash));
            }

            @Override
            public int hashCode() {
                return hash;
            }

            @Override
            public boolean equals(Object o) {
                if (!(o instanceof SimConfig)) return false;
                SimConfig s = (SimConfig)o;
                if (myEnergy != s.myEnergy) return false;
                else if (enemyEnergy != s.enemyEnergy) return false;
                else if (myDamageTaken != s.myDamageTaken) return false;
                else if (enemyDamageTaken != s.enemyDamageTaken) return false;
                else if (myHeat != s.myHeat) return false;
                else if (enemyHeat != s.enemyHeat) return false;
                else return true;
            }
        }
    }
}