/** * Copyright (C) 2013 Gundog Studios LLC. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package com.godsandtowers; import java.io.BufferedWriter; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import com.godsandtowers.core.GameInfo; import com.godsandtowers.core.PlayerStats; import com.godsandtowers.sprites.BaseCreature; import com.godsandtowers.sprites.BasePlayer; import com.godsandtowers.sprites.BaseRace; import com.godsandtowers.sprites.BaseTower; import com.godsandtowers.sprites.Player; import com.godsandtowers.sprites.Races; import com.gundogstudios.modules.DesktopAssets; import com.gundogstudios.modules.Modules; import com.gundogstudios.modules.SystemLogger; import com.gundogstudios.modules.basic.BasicPreferenceModule; import com.gundogstudios.modules.basic.EmptyProfilerModule; public class GeneticBalancer { private static final BasicPreferenceModule PREFERENCE_MODULE = new BasicPreferenceModule(); private static final CustomMessageModule MESSAGE_MODULE = new CustomMessageModule(); private static final ExecutorService THREAD_POOL = Executors.newFixedThreadPool(10); private static final int STARTING_LEVEL = 10; private static final int MAX_LEVEL = 100000; private static final float IDEAL_ENTROPY = .0000001f; private static final String FILE_NAME = "output/towers.txt"; private static final int MAX_MUTATIONS = 16; private static final int SAVE_MUTATIONS = 4; private static final int RUNS_PER_LEVEL = 10; public static void main(String[] args) throws IOException, InterruptedException, ExecutionException { long initialLoad = System.currentTimeMillis(); Modules.LOG = new SystemLogger(); Modules.MESSENGER = MESSAGE_MODULE; Modules.PREFERENCES = PREFERENCE_MODULE; Modules.PROFILER = new EmptyProfilerModule(); // PREFERENCE_MODULE.put(TDWPreferences.GAME_ENGINE_SPEED, 3); Modules.ASSETS = new DesktopAssets(); Mutation[] mutations = new Mutation[MAX_MUTATIONS]; Mutation m = new Mutation(); float lowestEntropy = m.run(); for (int i = 0; i < mutations.length; i++) { mutations[i] = new Mutation(m); } while (lowestEntropy > IDEAL_ENTROPY) { long runStart = System.currentTimeMillis(); for (Mutation mutation : mutations) { mutation.mutate(); float current = mutation.run(); if (current < lowestEntropy) { saveMutation(mutation); lowestEntropy = current; } } Arrays.sort(mutations); for (int i = SAVE_MUTATIONS; i < mutations.length; i++) { mutations[i].replace(mutations[i % SAVE_MUTATIONS]); } System.out.println("Lowest entropy " + lowestEntropy + " in " + (System.currentTimeMillis() - runStart) + " ms"); } THREAD_POOL.shutdown(); System.out.println("Took " + (System.currentTimeMillis() - initialLoad) + " ms to run"); } private static void saveMutation(Mutation mutation) throws IOException { BufferedWriter writer = new BufferedWriter(new FileWriter(FILE_NAME, true)); writer.append(mutation.toString()); writer.close(); } private static class Mutation implements Comparable<Mutation> { private Entropy entropy; private BasePlayer basePlayer; private BaseRace baseRace; private BaseTower[] baseTowers; private BaseCreature[] baseCreatures; private ArrayList<Result> results; public Mutation() { entropy = null; results = null; basePlayer = BasePlayer.getBasePlayer(); baseRace = BaseRace.getBaseRace(); baseTowers = BaseTower.getBaseTowers(); baseCreatures = BaseCreature.getBaseCreatures(); } public Mutation(Mutation mutation) { baseTowers = new BaseTower[mutation.baseTowers.length]; baseCreatures = new BaseCreature[mutation.baseCreatures.length]; replace(mutation); } public void replace(Mutation mutation) { entropy = mutation.entropy; results = mutation.results; basePlayer = new BasePlayer(mutation.basePlayer); baseRace = new BaseRace(mutation.baseRace); for (int i = 0; i < baseTowers.length; i++) { baseTowers[i] = new BaseTower(mutation.baseTowers[i]); } for (int i = 0; i < baseCreatures.length; i++) { baseCreatures[i] = new BaseCreature(mutation.baseCreatures[i]); } } public float run() throws InterruptedException, ExecutionException { long start = System.currentTimeMillis(); LinkedList<Future<GameInfo>> futures = submitFutures(); results = generateResults(futures); entropy = new Entropy(results); System.out.println("Entropy was " + entropy.getEntropy() + " in " + (System.currentTimeMillis() - start) + "\n"); return entropy.getEntropy(); } @Override public String toString() { StringBuilder builder = new StringBuilder(); builder.append("\n"); builder.append("Entropy: "); builder.append(entropy.getEntropy()); builder.append("\n"); for (int race : Races.ALL_RACES) { builder.append(Races.toString(race)); builder.append(" - "); builder.append(entropy.getRaceWinPercentage(race)); builder.append("\n"); } builder.append("\n"); for (BaseTower tower : baseTowers) { builder.append("new BaseTower("); builder.append(tower); builder.append("),\n"); } builder.append("\n"); builder.append("new BaseRace("); builder.append(baseRace); builder.append(");\n"); appendResults(builder); return builder.toString(); } private void appendResults(StringBuilder builder) { builder.append("\n\n"); builder.append("race,"); for (int level = 1; level <= MAX_LEVEL; level *= 10) { builder.append("level_" + level + ","); } builder.append("\n"); for (int races : Races.ALL_RACES) { for (int levelOne = STARTING_LEVEL; levelOne <= MAX_LEVEL; levelOne *= 10) { builder.append(Races.toString(races) + "_" + levelOne + ","); for (int levelTwo = STARTING_LEVEL; levelTwo <= MAX_LEVEL; levelTwo *= 10) { int wins = 0; int losses = 0; int draws = 0; for (Result result : results) { if (races == result.racesOne && levelOne == result.levelOne && levelTwo == result.levelTwo) { wins += result.wins; losses += result.losses; draws += result.draws; } } builder.append(wins + "-" + losses + "-" + draws + ","); } builder.append("\n"); } } } private LinkedList<Future<GameInfo>> submitFutures() { LinkedList<Future<GameInfo>> futures = new LinkedList<Future<GameInfo>>(); PlayerStats stats = new PlayerStats(1, basePlayer, baseRace, baseTowers, baseCreatures); for (int levelOne = STARTING_LEVEL; levelOne <= MAX_LEVEL; levelOne *= 10) { for (int racesOne : Races.ALL_RACES) { for (int levelTwo = STARTING_LEVEL; levelTwo <= MAX_LEVEL; levelTwo *= 10) { for (int racesTwo : Races.ALL_RACES) { if (racesOne == racesTwo || levelOne != levelTwo) continue; for (int i = 0; i < RUNS_PER_LEVEL; i++) { GameCallable callable = new GameCallable(MESSAGE_MODULE, stats, racesOne, levelOne, racesTwo, levelTwo); Future<GameInfo> future = THREAD_POOL.submit(callable); futures.add(future); } } } } } return futures; } private ArrayList<Result> generateResults(LinkedList<Future<GameInfo>> futures) throws InterruptedException, ExecutionException { ArrayList<Result> results = new ArrayList<Result>(); for (int levelOne = STARTING_LEVEL; levelOne <= MAX_LEVEL; levelOne *= 10) { for (int racesOne : Races.ALL_RACES) { for (int levelTwo = STARTING_LEVEL; levelTwo <= MAX_LEVEL; levelTwo *= 10) { for (int racesTwo : Races.ALL_RACES) { if (racesOne == racesTwo || levelOne != levelTwo) continue; Result result = new Result(racesOne, levelOne, racesTwo, levelTwo); for (int i = 0; i < RUNS_PER_LEVEL; i++) { Future<GameInfo> future = futures.removeFirst(); GameInfo gameInfo = future.get(); Player[] players = gameInfo.getPlayers(); if (players[0].getLife() > 0 && players[1].getLife() <= 0) { result.wins++; } else if (players[0].getLife() <= 0 && players[1].getLife() > 0) { result.losses++; } else { result.draws++; } } results.add(result); } } } } return results; } public void mutate() { float percentage = entropy.getRaceWinPercentage(Races.ICE) - .5f; float slowDuration = baseRace.getSlowDuration() * (float) (1f - (Math.random() * percentage)); float slowFactor = baseRace.getSlowFactor() * (float) (1f - (Math.random() * percentage * -1f)); float creatureEvadePercentage = baseRace.getCreatureEvadePercentage() * (float) (1f - (Math.random() * percentage)); percentage = entropy.getRaceWinPercentage(Races.WIND) - .5f; float stunDuration = baseRace.getStunDuration() * (float) (1f - (Math.random() * percentage)); float towerAttackRateModifier = baseRace.getTowerAttackRateModifier() * (float) (1f - (Math.random() * percentage)); float creatureSpeedModifier = baseRace.getCreatureSpeedModifier() * (float) (1f - (Math.random() * percentage)); percentage = entropy.getRaceWinPercentage(Races.EARTH) - .5f; float towerDamageModifier = baseRace.getTowerDamageModifier() * (float) (1f - (Math.random() * percentage)); float towerDefenseModifier = baseRace.getTowerDefenseModifier() * (float) (1f - (Math.random() * percentage)); float creatureDefenseModifier = baseRace.getCreatureDefenseModifier() * (float) (1f - (Math.random() * percentage)); percentage = entropy.getRaceWinPercentage(Races.FIRE) - .5f; float splashRadius = baseRace.getSplashRadius() * (float) (1f - (Math.random() * percentage)); float splashStrengthFade = baseRace.getSplashStrengthFade() * (float) (1f - (Math.random() * percentage)); float creatureDamageModifier = baseRace.getCreatureDamageModifier() * (float) (1f - (Math.random() * percentage)); percentage = entropy.getRaceWinPercentage(Races.LIFE) - .5f; float drainDuration = baseRace.getDrainDuration() * (float) (1f - (Math.random() * percentage)); float drainPercentage = baseRace.getDrainPercentage() * (float) (1f - (Math.random() * percentage)); float healPercentage = baseRace.getHealPercentage() * (float) (1f - (Math.random() * percentage)); percentage = entropy.getRaceWinPercentage(Races.DEATH) - .5f; float killPercentage = baseRace.getKillPercentage() * (float) (1f - (Math.random() * percentage)); float creatureHealthModifier = baseRace.getCreatureHealthModifier() * (float) (1f - (Math.random() * percentage)); float resurrectPercentage = baseRace.getResurrectPercentage() * (float) (1f - (Math.random() * percentage)); baseRace = new BaseRace(slowDuration, slowFactor, creatureEvadePercentage, stunDuration, towerAttackRateModifier, creatureSpeedModifier, towerDamageModifier, towerDefenseModifier, creatureDefenseModifier, splashRadius, splashStrengthFade, creatureDamageModifier, drainDuration, drainPercentage, healPercentage, killPercentage, creatureHealthModifier, resurrectPercentage); for (int i = 0; i < baseTowers.length; i++) { String name = baseTowers[i].getName(); int races = baseTowers[i].getRaces(); if (Races.getNumRaces(races) > 1) continue; percentage = entropy.getRaceWinPercentage(races) - .5f; float factor = (float) (Math.random() * percentage); float damage = baseTowers[i].getDamage() * (1 - factor); factor = (float) (Math.random() * percentage) * -1f; float attackRate = baseTowers[i].getAttackRate() * (1 - factor); float cost = baseTowers[i].getCost(); float health = baseTowers[i].getHealth(); float attackRange = baseTowers[i].getAttackRange(); float defense = baseTowers[i].getDefense(); boolean attacksGround = baseTowers[i].attacksGround(); boolean attacksAir = baseTowers[i].attacksAir(); boolean attacksAllInRange = baseTowers[i].attacksAllInRange(); boolean attacksInstantly = baseTowers[i].attacksInstantly(); boolean unlocked = baseTowers[i].isUnlocked(); baseTowers[i] = new BaseTower(name, races, cost, damage, health, defense, attackRate, attackRange, attacksGround, attacksAir, attacksAllInRange, attacksInstantly, unlocked); } } @Override public int compareTo(Mutation o) { return Float.floatToIntBits(o.entropy.getEntropy()) == Float.floatToIntBits(entropy.getEntropy()) ? 0 : entropy.getEntropy() > o.entropy.getEntropy() ? 1 : -1; } } private static class Entropy { private HashMap<Integer, Float> raceWinPercentage = new HashMap<Integer, Float>(); private float entropy = 0f; public Entropy(ArrayList<Result> results) { for (int races : Races.ALL_RACES) { float wins = 0; float losses = 0; // float draws = 0; for (Result result : results) { if (result.levelOne == result.levelTwo) { if (races == result.racesOne) { wins += result.wins; losses += result.losses; // draws += result.draws; } else if (races == result.racesTwo) { wins += result.losses; losses += result.wins; // draws += result.draws; } } } float winPercentage = (wins) / (wins + losses); raceWinPercentage.put(races, winPercentage); System.out.println(Races.toString(races) + " - " + winPercentage + " " + wins + "-" + losses); entropy += Math.pow(winPercentage - .5f, 2); } } public float getRaceWinPercentage(int race) { return raceWinPercentage.get(race); } public float getEntropy() { return entropy; } } }