package animals;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;

/*public*/ class DeepWolf extends Animal {

    private static final int                    NUM_WOLF_SPECIES            = (int) Math.round(Math.pow(
                                                                                    ((double) MAP_SIZE) / 20, 2) - 3) - 3;
    private static final int                    POPULATION                  = 100;
    private static int                          MAP_AREA;

    private static final char                   EMPTY                       = ' ';
    private static final char                   STONE                       = 'S';
    private static final char                   BEAR                        = 'B';
    private static final char                   LION                        = 'L';
    private static final char                   WOLF                        = 'W';
    private static final char                   ENEMY_WOLF                  = 'w';
    private static final char                   FRIENDLY_WOLF               = '@';

    private static final Attack[]               ATTACKS                     = { Attack.ROCK, Attack.PAPER,
            Attack.SCISSORS                                             };
    private static final float                  EXPECTED_WIN_PERCENTAGE     = 1f / ATTACKS.length;

    private static final float                  POTENTIAL_DANGER_DECAY      = 63 / 64f;
    private static final float                  PROXIMITY_DANGER_DECAY      = 3 / 4f;
    private static final float                  UNSEEN_DEATH_BASE_CHANCE    = 1 / 16f;

    public static final Random                  random                      = new Random();

    private static final FightIntel             FIGHT_INTEL_OVERRIDE        = new FightIntel();
    static {
        FIGHT_INTEL_OVERRIDE.registerWin(STONE, Attack.PAPER);
        FIGHT_INTEL_OVERRIDE.registerWin(BEAR, Attack.SCISSORS);
        FIGHT_INTEL_OVERRIDE.registerWin(LION, Attack.SCISSORS);
        FIGHT_INTEL_OVERRIDE.registerWin(LION, Attack.SCISSORS);
        FIGHT_INTEL_OVERRIDE.registerWin(LION, Attack.SCISSORS);
        FIGHT_INTEL_OVERRIDE.registerLoss(LION, Attack.SCISSORS);
    }

    private static final FightIntel             fightIntel                  = new FightIntel();
    private static final PopulationIntel        populationIntel             = new PopulationIntel();
    static {
        populationIntel.addMany(STONE, POPULATION);
        populationIntel.addMany(BEAR, POPULATION);
        populationIntel.addMany(LION, POPULATION);
        populationIntel.addMany(WOLF, (NUM_WOLF_SPECIES - 1) * POPULATION);
    }

    private static MoveIntel                    movesThisTurn;

    private static final Collection<DeepWolf>   us                          = new ArrayList<DeepWolf>();

    private static int                          turn                        = -1;
    private static boolean                      movePhase                   = false;

    private boolean                             calledToMoveThisTurn        = false;
    private final List<Fight>                   fightsLastTurn              = new ArrayList<Fight>();

    private int                                 y                           = 0;
    private int                                 x                           = 0;

    private final PositionIntel                 positionIntel               = new PositionIntel();

    // for debug purposes
    private BetterMove                          moveThisTurn;
    private Map<BetterMove, List<Double>>       dangerThisTurn;
    private List<Double>                        dangerTemp;

    public DeepWolf() {
        super(WOLF);
        us.add(this);
        populationIntel.addOne(FRIENDLY_WOLF);
        MAP_AREA = MAP_SIZE * MAP_SIZE;
    }

    @Override
    public Attack fight(final char opponent) {
        if (movePhase) {
            enterFightPhase();
        }

        Attack attack = doFight(opponent);
        fightsLastTurn.add(new Fight(opponent, attack));

        return attack;
    }

    private Attack doFight(final char opponent) {
        return getFightIntel(opponent).getBest();
    }

    private static SimpleFightIntel getFightIntel(final char opponent) {
        if (FIGHT_INTEL_OVERRIDE.hasData(opponent)) {
            return FIGHT_INTEL_OVERRIDE.getOpponentIntel(opponent);
        } else {
            return fightIntel.getOpponentIntel(opponent);
        }
    }

    @Override
    public Move move() {
        if (calledToMoveThisTurn) {
            enterFightPhase();
        }
        if (!movePhase) {
            enterMovePhase();
        }

        calledToMoveThisTurn = true;

        surroundings[1][1] = EMPTY;

        for (int row = 0; row < 3; row++) {
            for (int col = 0; col < 3; col++) {
                positionIntel.add(y + (row - 1), x + (col - 1), surroundings[row][col]);
            }
        }

        BetterMove move = doMove();
        movesThisTurn.add(move);
        moveThisTurn = move;

        y = clip(y + move.dy);
        x = clip(x + move.dx);

        return move.toMove();
    }

    private BetterMove doMove() {
        double leastDanger = Float.POSITIVE_INFINITY;
        BetterMove leastDangerousMove = null;

        dangerThisTurn = new HashMap<BetterMove, List<Double>>();

        for (BetterMove move : BetterMove.values()) {
            dangerTemp = new ArrayList<Double>(5);
            for (int i = 0; i < 5; i++) {
                dangerTemp.add(0d);
            }

            double danger = positionIntel.assessDangerOnTurn(y + move.dy, x + move.dx, turn + 1);

            if (move.hasOpposite()) {
                double friendlyWolfCollideProbability = (movesThisTurn.percentageOf(move.getOpposite()) * (us.size() - 1))
                        / MAP_AREA;
                dangerTemp.set(4, friendlyWolfCollideProbability * getOpponentDanger(WOLF));
                danger += friendlyWolfCollideProbability * getOpponentDanger(WOLF);
            }

            dangerThisTurn.put(move, dangerTemp);

            if (danger < leastDanger) {
                leastDanger = danger;
                leastDangerousMove = move;
            }
        }

        if (leastDangerousMove != null) {
            return leastDangerousMove;
        } else {
            throw new AssertionError();
        }
    }

    private static void enterMovePhase() {
        movePhase = true;
        turn++;

        movesThisTurn = new MoveIntel();
    }

    private static void enterFightPhase() {
        movePhase = false;

        Iterator<DeepWolf> wolves = us.iterator();
        while (wolves.hasNext()) {
            DeepWolf wolf = wolves.next();

            Iterator<Fight> fights = wolf.fightsLastTurn.iterator();
            while (fights.hasNext()) {
                Fight fight = fights.next();
                char opponent = fight.getOpponent();
                Attack attack = fight.getAttack();

                if (fights.hasNext() || wolf.calledToMoveThisTurn) {
                    fightIntel.registerWin(opponent, attack);
                    // TODO: don't assume that we killed an unfriendly wolf here?
                    populationIntel.removeOne(opponent);
                } else {
                    fightIntel.registerLoss(opponent, attack);
                    populationIntel.removeOne(FRIENDLY_WOLF);
                    wolves.remove();
                }
            }

            wolf.calledToMoveThisTurn = false;
            wolf.fightsLastTurn.clear();
        }
    }

    private static int clip(final int n) {
        return ((n % MAP_SIZE) + MAP_SIZE) % MAP_SIZE;
    }

    private static int posDist(final int m, final int n) {
        return clip(n - m);
    }

    private static int dist(final int m, final int n) {
        int posDist = posDist(m, n);
        return posDist < (MAP_SIZE / 2) ? posDist : MAP_SIZE - posDist;
    }

    private boolean isVisible(final int y, final int x) {
        return (dist(y, this.y) <= 1) && (dist(x, this.x) <= 1);
    }

    private static double getOpponentDanger(final char opponent) {
        double winPercentage;

        if (opponent == FRIENDLY_WOLF) {
            winPercentage = 0;

        } else {
            SimpleFightIntel opponentFightIntel = getFightIntel(opponent);
            winPercentage = opponentFightIntel.getWinPercentage(opponentFightIntel.getBest());

            if (winPercentage == 0) {
                winPercentage = EXPECTED_WIN_PERCENTAGE;
            }
            if (opponent == WOLF) {
                winPercentage *= (double) (NUM_WOLF_SPECIES - 1) / NUM_WOLF_SPECIES;
            }
        }

        return (1 / winPercentage) - 1;
    }

    private static double decayPotentialDanger(final double danger, final int turnsStale) {
        return decayDanger(danger, turnsStale, POTENTIAL_DANGER_DECAY);
    }

    private static double decayProximityDanger(final double danger, final int proximity) {
        return decayDanger(danger, proximity, PROXIMITY_DANGER_DECAY);
    }

    private static double decayDanger(final double danger, final int decay, final double factor) {
        return danger * Math.pow(factor, decay);
    }

    private static class Fight {

        private final char      opponent;
        private final Attack    attack;

        Fight(final char opponent, final Attack attack) {
            this.opponent = opponent;
            this.attack = attack;
        }

        private char getOpponent() {
            return opponent;
        }

        private Attack getAttack() {
            return attack;
        }

    }

    private static class WinLossIntel {

        private int total   = 0;
        private int wins    = 0;

        WinLossIntel() {}

        private int getTotal() {
            return total;
        }

        private int getWins() {
            return wins;
        }

        private int getLosses() {
            return total - wins;
        }

        private double getWinPercentage() {
            return total == 0 ? 0 : (double) wins / total;
        }

        private void registerWin() {
            total++;
            wins++;
        }

        private void registerLoss() {
            total++;
        }

        private void registerResult(final boolean won) {
            if (won) {
                registerWin();
            } else {
                registerLoss();
            }
        }

        @Override
        public String toString() {
            return wins + "/" + total;
        }
    }

    private static class SimpleFightIntel {

        private final Map<Attack, WinLossIntel> attackIntel;

        SimpleFightIntel() {
            attackIntel = new HashMap<Attack, WinLossIntel>();
            for (Attack attack : ATTACKS) {
                get(attack);
            }
        }

        private WinLossIntel get(final Attack attack) {
            WinLossIntel result = attackIntel.get(attack);
            if (result == null) {
                result = new WinLossIntel();
                attackIntel.put(attack, result);
            }
            return result;
        }

        private int getTotal(final Attack attack) {
            return get(attack).getTotal();
        }

        private int getWins(final Attack attack) {
            return get(attack).getWins();
        }

        private int getLosses(final Attack attack) {
            return get(attack).getLosses();
        }

        private double getWinPercentage(final Attack attack) {
            return get(attack).getWinPercentage();
        }

        private Attack getBest() {
            Attack bestAttack = null;
            double bestPercentage = -1;
            List<Attack> leastUsedAttacks = new ArrayList<Attack>(ATTACKS.length);
            int leastUses = Integer.MAX_VALUE;

            for (Entry<Attack, WinLossIntel> entry : attackIntel.entrySet()) {
                Attack attack = entry.getKey();
                WinLossIntel intel = entry.getValue();

                double percentage = entry.getValue().getWinPercentage();
                if (percentage > bestPercentage) {
                    bestAttack = entry.getKey();
                    bestPercentage = percentage;
                }

                int uses = intel.getTotal();
                if (uses <= leastUses) {
                    leastUsedAttacks.add(attack);
                    leastUses = uses;
                }
            }

            if (((bestPercentage - EXPECTED_WIN_PERCENTAGE) / (1 - EXPECTED_WIN_PERCENTAGE)) < (1.0 / (leastUses + 2 + random
                    .nextDouble()))) {
                return leastUsedAttacks.get(random.nextInt(leastUsedAttacks.size()));
            } else {
                return bestAttack;
            }
        }

        private void registerWin(final Attack attack) {
            registerResult(attack, true);
        }

        private void registerLoss(final Attack attack) {
            registerResult(attack, false);
        }

        private void registerResult(final Attack attack, final boolean won) {
            get(attack).registerResult(won);
        }

        private boolean hasData() {
            for (WinLossIntel intel : attackIntel.values()) {
                if (intel.getTotal() > 0) {
                    return true;
                }
            }
            return false;
        }

        @Override
        public String toString() {
            return attackIntel.toString();
        }
    }

    private static class FightIntel {

        private final Map<Character, SimpleFightIntel>  fightIntel  = new HashMap<Character, SimpleFightIntel>();

        FightIntel() {}

        private SimpleFightIntel get(final Character opponent) {
            SimpleFightIntel result = fightIntel.get(opponent);
            if (result == null) {
                result = new SimpleFightIntel();
                fightIntel.put(opponent, result);
            }
            return result;
        }

        private SimpleFightIntel getOpponentIntel(final Character opponent) {
            return get(opponent);
        }

        private WinLossIntel get(final Character opponent, final Attack attack) {
            return get(opponent).get(attack);
        }

        private int getTotal(final Character opponent, final Attack attack) {
            return get(opponent, attack).getTotal();
        }

        private int getWins(final Character opponent, final Attack attack) {
            return get(opponent, attack).getWins();
        }

        private int getLosses(final Character opponent, final Attack attack) {
            return get(opponent, attack).getLosses();
        }

        private double getWinPercentage(final Character opponent, final Attack attack) {
            return get(opponent, attack).getWinPercentage();
        }

        private Attack getBest(final Character opponent) {
            return get(opponent).getBest();
        }

        private void registerWin(final Character opponent, final Attack attack) {
            registerResult(opponent, attack, true);
        }

        private void registerLoss(final Character opponent, final Attack attack) {
            registerResult(opponent, attack, false);
        }

        private void registerResult(final Character opponent, final Attack attack, final boolean won) {
            get(opponent, attack).registerResult(won);
        }

        private boolean hasData(final Character opponent) {
            return get(opponent).hasData();
        }

        @Override
        public String toString() {
            return fightIntel.toString();
        }

    }

    private static class Coordinate {

        private final int   x;
        private final int   y;

        Coordinate(final int y, final int x) {
            this.x = clip(x);
            this.y = clip(y);
        }

        private int getX() {
            return x;
        }

        private int getY() {
            return y;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = (prime * result) + x;
            result = (prime * result) + y;
            return result;
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            Coordinate other = (Coordinate) obj;
            return (x == other.x) && (y == other.y);
        }

        @Override
        public String toString() {
            return "[" + y + "," + x + "]";
        }

    }

    private static class LionIntel {

        private final Map<Coordinate, Integer>  lionIntel   = new HashMap<Coordinate, Integer>();

        LionIntel() {}

        private static Coordinate getCoordinateOnTurn(final Coordinate coordinate, final int onTurn) {
            return getCoordinateOnTurn(coordinate.getY(), coordinate.getX(), onTurn);
        }

        private static Coordinate getCoordinateOnTurn(final int y, final int x, final int onTurn) {
            return getCoordinateOnTurnRelativeToTurn(y, x, onTurn, 0);
        }

        private static Coordinate getCoordinateOnTurnRelativeToTurn(final int y, final int x, final int onTurn,
                final int relativeToTurn) {
            return new Coordinate(y + ((onTurn / 2) - (relativeToTurn / 2)), x
                    + (((onTurn + 1) / 2) - ((relativeToTurn + 1) / 2)));
        }

        private void addOnTurn(final int y, final int x, final int onTurn) {
            lionIntel.put(getCoordinateOnTurnRelativeToTurn(y, x, 0, onTurn), onTurn);
        }

        private void add(final int y, final int x) {
            addOnTurn(y, x, turn);
        }

        private void add(final int y, final int x, final boolean isPresent) {
            if (isPresent) {
                add(y, x);
            } else {
                remove(y, x);
            }
        }

        private void remove(final int y, final int x) {
            lionIntel.remove(getCoordinateOnTurnRelativeToTurn(y, x, 0, turn));
        }

        private int size() {
            return lionIntel.size();
        }

        private double assessDangerOnTurn(final int y, final int x, final int onTurn) {
            return assessDangerOnTurn(y, x, onTurn, false);
        }

        private double assessDangerOnTurnWithFuture(final int y, final int x, final int onTurn) {
            return assessDangerOnTurn(y, x, onTurn, true);
        }

        private double assessDangerOnTurn(final int y, final int x, final int onTurn, final boolean withFuture) {
            double danger = 0;

            for (Entry<Coordinate, Integer> entry : lionIntel.entrySet()) {
                Coordinate lion = entry.getKey();
                int turnLastSeen = entry.getValue();
                Coordinate lionWhenLastSeen = getCoordinateOnTurn(lion, turnLastSeen);

                int dy = posDist(lionWhenLastSeen.getY(), y);
                int dx = posDist(lionWhenLastSeen.getX(), x);
                if ((dy == 0) && (dx == 0)) {
                    dy = dx = MAP_SIZE;
                } else if ((dy == 0) && (dx == (MAP_SIZE - 1))) {
                    dy = MAP_SIZE;
                } else if ((dx == 0) && (dy == (MAP_SIZE - 1))) {
                    dx = MAP_SIZE;
                }

                int turnsUntilClosestEncounterY = (dy * 2) - (turnLastSeen % 2);
                int turnsUntilClosestEncounterX = ((dx * 2) - ((turnLastSeen + 1) % 2));

                int turnsUntilClosestEncounter = Math.min(turnsUntilClosestEncounterY, turnsUntilClosestEncounterX) + 1;
                Coordinate lionAtClosestEncounter = getCoordinateOnTurn(lion, turnLastSeen + turnsUntilClosestEncounter);
                int closestEncounterDist = dist(lionAtClosestEncounter.getY(), y)
                        + dist(lionAtClosestEncounter.getX(), x);

                if (((turnLastSeen + turnsUntilClosestEncounter) - onTurn) < closestEncounterDist) {
                    turnsUntilClosestEncounter = Math.max(turnsUntilClosestEncounterY, turnsUntilClosestEncounterX);
                    lionAtClosestEncounter = getCoordinateOnTurn(lion, turnLastSeen + turnsUntilClosestEncounter);
                    closestEncounterDist = dist(lionAtClosestEncounter.getY(), y)
                            + dist(lionAtClosestEncounter.getX(), x);
                }

                if (withFuture || (((turnsUntilClosestEncounter == 1)) && (closestEncounterDist == 0))) {
                    double lionDanger = decayProximityDanger(
                            decayPotentialDanger(getOpponentDanger(LION), turnsUntilClosestEncounter),
                            closestEncounterDist);
                    danger += lionDanger;
                }
            }

            return danger;
        }

        @Override
        public int hashCode() {
            return lionIntel.hashCode();
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            LionIntel other = (LionIntel) obj;
            return lionIntel == other.lionIntel;
        }

        @Override
        public String toString() {
            return LionIntel.class.getSimpleName() + "[" + lionIntel.toString() + "]";
        }

    }

    private static class WolfIntel {

        private final Map<Coordinate, Integer>  wolfIntel   = new HashMap<Coordinate, Integer>();

        WolfIntel() {}

        private void addOnTurn(final int y, final int x, final int onTurn) {
            wolfIntel.put(new Coordinate(y, x), onTurn);
        }

        private void add(final int y, final int x) {
            addOnTurn(y, x, turn);
        }

        private void add(final int y, final int x, final boolean isPresent) {
            if (isPresent) {
                add(y, x);
            } else {
                remove(y, x);
            }
        }

        private void remove(final int y, final int x) {
            wolfIntel.remove(new Coordinate(y, x));
        }

        private int size() {
            return wolfIntel.size();
        }

        private double assessDangerOnTurn(final int y, final int x, final int onTurn) {
            double danger = 0;

            for (Entry<Coordinate, Integer> entry : wolfIntel.entrySet()) {
                Coordinate wolf = entry.getKey();
                int turnsStale = onTurn - entry.getValue();
                int dist = dist(x, wolf.getX()) + dist(y, wolf.getY());

                // if (turnsStale >= dist) {
                // danger += decayPotentialDanger(getOpponentDanger(WOLF) * Math.pow(.75, dist - 1), turnsStale - 1);
                // }
                if ((turnsStale >= dist) && (dist <= 1)) {
                    danger += decayPotentialDanger(getOpponentDanger(WOLF) * Math.pow(PROXIMITY_DANGER_DECAY, dist),
                            turnsStale - 1);
                }
            }

            return danger;
        }

        @Override
        public int hashCode() {
            return wolfIntel.hashCode();
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            WolfIntel other = (WolfIntel) obj;
            return wolfIntel == other.wolfIntel;
        }

        @Override
        public String toString() {
            return WolfIntel.class.getSimpleName() + "[" + wolfIntel.toString() + "]";
        }

    }

    private static class MoveIntel {

        private final Map<BetterMove, Integer>  moves   = new HashMap<BetterMove, Integer>();
        private int                             total   = 0;

        MoveIntel() {}

        private int get(final BetterMove move) {
            Integer result = moves.get(move);
            if (result == null) {
                return 0;
            } else {
                return result;
            }
        }

        private void add(final BetterMove move) {
            moves.put(move, get(move) + 1);
            total++;
        }

        private int size() {
            return total;
        }

        private double percentageOf(final BetterMove move) {
            int moveCount = get(move);
            return moveCount == 0 ? 0 : ((double) moveCount) / total;
        }

    }

    private class PositionIntel {

        private final LionIntel lionIntel   = new LionIntel();
        private final WolfIntel wolfIntel   = new WolfIntel();

        PositionIntel() {}

        private void add(final int y, final int x, final char animal) {
            lionIntel.add(y, x, animal == LION);
            wolfIntel.add(y, x, animal == WOLF);
        }

        private double assessDangerOnTurn(final int y, final int x, final int onTurn) {
            LionIntel potentialLionIntel = new LionIntel();
            WolfIntel potentialWolfIntel = new WolfIntel();

            for (BetterMove move : BetterMove.values()) {
                int potentialY = y + move.dy;
                int potentialX = x + move.dx;
                if (!isVisible(potentialY, potentialX)) {
                    potentialLionIntel.addOnTurn(potentialY, potentialX, onTurn - 1);
                    potentialWolfIntel.addOnTurn(potentialY, potentialX, onTurn - 1);
                }
            }

            double potentialLionDanger = potentialLionIntel.assessDangerOnTurn(y, x, onTurn)
                    * (Math.max(0, populationIntel.guessOnTurn(LION, onTurn - 1) - lionIntel.size()) / MAP_AREA);
            double potentialWolfDanger = potentialWolfIntel.assessDangerOnTurn(y, x, onTurn)
                    * (Math.max(
                            0,
                            populationIntel.guessOnTurn(WOLF, onTurn - 1)
                                    + (populationIntel.guessOnTurn(FRIENDLY_WOLF, onTurn - 1) - 1)) / MAP_AREA);
            double lionDanger = lionIntel.assessDangerOnTurnWithFuture(y, x, onTurn);
            double wolfDanger = wolfIntel.assessDangerOnTurn(y, x, onTurn);

            dangerTemp.set(0, potentialLionDanger);
            dangerTemp.set(1, potentialWolfDanger);
            dangerTemp.set(2, lionDanger);
            dangerTemp.set(3, wolfDanger);

            return potentialLionDanger + potentialWolfDanger + lionDanger + wolfDanger;
        }

        @Override
        public int hashCode() {
            final int prime = 31;
            int result = 1;
            result = (prime * result) + lionIntel.hashCode();
            result = (prime * result) + wolfIntel.hashCode();
            return result;
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            PositionIntel other = (PositionIntel) obj;
            return lionIntel.equals(other.lionIntel) && wolfIntel.equals(other.wolfIntel);
        }

        @Override
        public String toString() {
            return PositionIntel.class.getSimpleName() + "[" + lionIntel.toString() + ", " + wolfIntel.toString() + "]";
        }
    }

    private static class PopulationIntel {

        private final Map<Character, Integer>   populations = new HashMap<Character, Integer>();

        PopulationIntel() {}

        private int get(final char animal) {
            Integer result = populations.get(animal);
            if (result == null) {
                return 0;
            } else {
                return result;
            }
        }

        private int getMax(final char animal) {
            return get(animal);
        }

        private void addOne(final char animal) {
            addMany(animal, 1);
        }

        private void addMany(final char animal, final int n) {
            populations.put(animal, get(animal) + n);
        }

        private void removeOne(final char animal) {
            removeMany(animal, 1);
        }

        private void removeMany(final char animal, final int n) {
            populations.put(animal, get(animal) - n);
        }

        private double guess(final char animal) {
            return guessOnTurn(animal, turn);
        }

        private double guessOnTurn(final char animal, final int onTurn) {
            int max = get(animal);

            switch (animal) {
            case FRIENDLY_WOLF:
                return max;
            default:
                return max * Math.pow(1 - Math.pow(UNSEEN_DEATH_BASE_CHANCE, (getOpponentDanger(animal) + 1)), onTurn);
            }
        }

        @Override
        public int hashCode() {
            return populations.hashCode();
        }

        @Override
        public boolean equals(final Object obj) {
            if (this == obj) {
                return true;
            }
            if (obj == null) {
                return false;
            }
            if (getClass() != obj.getClass()) {
                return false;
            }
            PopulationIntel other = (PopulationIntel) obj;
            return populations.equals(other.populations);
        }

        @Override
        public String toString() {
            return PopulationIntel.class.getSimpleName() + "[" + populations + "]";
        }

    }

    private static enum BetterMove {
        UP(0, -1), RIGHT(1, 0), DOWN(0, 1), LEFT(-1, 0), HOLD(0, 0);

        public final int    dx;
        public final int    dy;

        BetterMove(final int dx, final int dy) {
            this.dx = dx;
            this.dy = dy;
        }

        public Move toMove() {
            return toMove(this);
        }

        public static Move toMove(final BetterMove move) {
            return Move.valueOf(move.name());
        }

        public static BetterMove fromMove(final Move move) {
            return BetterMove.valueOf(move.name());
        }

        public boolean hasOpposite() {
            return hasOpposite(this);
        }

        public static boolean hasOpposite(final BetterMove move) {
            return getOpposite(move) != null;
        }

        public BetterMove getOpposite() {
            return getOpposite(this);
        }

        public static BetterMove getOpposite(final BetterMove move) {
            switch (move) {
            case UP:
                return DOWN;
            case RIGHT:
                return LEFT;
            case DOWN:
                return UP;
            case LEFT:
                return RIGHT;
            case HOLD:
                return null;
            default:
                throw new AssertionError();
            }
        }
    }

}