import empire_ai.weasel.WeaselAI; import util.design_export; import util.random_designs; interface RaceDesigns { bool preCompose(DesignTarget@ target); bool postCompose(DesignTarget@ target); bool design(DesignTarget@ target, int size, const Design@& output); }; enum DesignPurpose { DP_Scout, DP_Combat, DP_Defense, DP_Support, DP_Gate, DP_Slipstream, DP_Mothership, DP_Miner, DP_COUNT, DP_Unknown, }; tidy final class DesignTarget { int id = -1; const Design@ active; string customName; array potential; array scores; uint purpose; double targetBuildCost = 0; double targetMaintenance = 0; double targetLaborCost = 0; double dps = 0.0; double hp = 0.0; double supplyDrain = 0.0; double targetSize = 0; bool findSize = false; Designer@ designer; DesignTarget() { } DesignTarget(uint type, double targetSize) { this.purpose = type; this.targetSize = targetSize; } uint get_designType() { switch(purpose) { case DP_Scout: return DT_Flagship; case DP_Combat: return DT_Flagship; case DP_Defense: return DT_Station; case DP_Support: return DT_Support; case DP_Gate: return DT_Station; case DP_Slipstream: return DT_Flagship; case DP_Mothership: return DT_Flagship; } return DT_Flagship; } void save(Designs& designs, SaveFile& file) { if(active !is null) { file.write1(); file << active; file << dps; file << supplyDrain; file << hp; } else { file.write0(); } file << purpose; file << targetBuildCost; file << targetMaintenance; file << targetLaborCost; file << targetSize; file << findSize; file << customName; } void load(Designs& designs, SaveFile& file) { if(file.readBit()) { file >> active; file >> dps; file >> supplyDrain; file >> hp; } file >> purpose; file >> targetBuildCost; file >> targetMaintenance; file >> targetLaborCost; file >> targetSize; file >> findSize; file >> customName; } void prepare(AI& ai) { @designer = Designer(designType, targetSize, ai.empire, compose=false); designer.randomHull = true; switch(purpose) { case DP_Scout: designer.composeScout(); break; case DP_Combat: designer.composeFlagship(); break; case DP_Defense: designer.composeStation(); break; case DP_Support: designer.composeSupport(); break; case DP_Gate: designer.composeGate(); break; case DP_Slipstream: designer.composeSlipstream(); break; case DP_Mothership: designer.composeMothership(); break; } } double weight(double value, double goal) { if(value < goal) return sqr(value / goal); else if(value > goal * 1.5) return goal / value; return 1.0; } double costWeight(double value, double goal) { if(findSize) { if(value < goal) return 1.0; else return 0.000001; } else { if(value < goal) return goal / value; else return pow(0.2, ((value / goal) - 1.0) * 10.0); } } double evaluate(AI& ai, const Design& dsg) { double w = 1.0; //Try to stick as close to our target as we can if(targetBuildCost != 0) w *= costWeight(dsg.total(HV_BuildCost), targetBuildCost); if(targetLaborCost != 0) w *= costWeight(dsg.total(HV_LaborCost), targetLaborCost); if(targetMaintenance != 0) w *= costWeight(dsg.total(HV_MaintainCost), targetMaintenance); double predictHP = 0.0; double predictDPS = 0.0; double predictDrain = 0.0; //Value support capacity where appropriate if(purpose == DP_Combat) { double supCap = dsg.total(SV_SupportCapacity); double avgHP = 0, avgDPS = 0, avgDrain = 0.0; cast(ai.designs).getSupportAverages(avgHP, avgDPS, avgDrain); predictHP += supCap * avgHP; predictDPS += supCap * avgDPS; predictDrain += supCap * avgDrain; } //Value combat strength where appropriate if(purpose != DP_Scout && purpose != DP_Slipstream && purpose != DP_Mothership) { predictDPS += dsg.total(SV_DPS); predictHP += dsg.totalHP + dsg.total(SV_ShieldCapacity); predictDrain += dsg.total(SV_SupplyDrain); if(purpose != DP_Support) { w *= (predictHP * predictDPS) * 0.001; double supplyStores = dsg.total(SV_SupplyCapacity); double actionTime = supplyStores / predictDrain; w *= weight(actionTime, ai.behavior.fleetAimSupplyDuration); } } //Value acceleration on a target if(purpose != DP_Defense && purpose != DP_Gate) { double targetAccel = 2.0; if(purpose == DP_Support) targetAccel *= 1.5; else if(purpose == DP_Scout) targetAccel *= 3.0; w *= weight(dsg.total(SV_Thrust) / max(dsg.total(HV_Mass), 0.01), targetAccel); } //Penalties for having important systems easy to shoot down uint holes = 0; for(uint i = 0, cnt = dsg.subsystemCount; i < cnt; ++i) { auto@ sys = dsg.subsystem(i); if(!sys.type.hasTag(ST_Important)) continue; //TODO: We should be able to penalize for exposed supply storage if(sys.type.hasTag(ST_NoCore)) continue; vec2u core = sys.core; for(uint d = 0; d < 6; ++d) { if(!traceContainsArmor(dsg, core, d)) holes += 1; } } if(holes != 0) w /= pow(0.9, double(holes)); //TODO: Check FTL return w; } bool traceContainsArmor(const Design@ dsg, const vec2u& startPos, uint direction) { vec2u pos = startPos; while(dsg.hull.active.valid(pos)) { if(!dsg.hull.active.advance(pos, HexGridAdjacency(direction))) break; auto@ sys = dsg.subsystem(pos.x, pos.y); if(sys is null) continue; if(sys.type.hasTag(ST_IsArmor)) return true; } return false; } bool contains(const Design& dsg) { if(active is null) return false; if(dsg.mostUpdated() is active.mostUpdated()) return true; return false; } const Design@ design(AI& ai, Designs& designs) { int trySize = targetSize; if(findSize) { trySize = randomd(0.75, 1.25) * targetSize; trySize = 5 * round(double(designer.size) / 5.0); } if(designs.race !is null) { const Design@ fromRace; if(designs.race.design(this, trySize, fromRace)) return fromRace; } if(designer !is null) { designer.size = trySize; return designer.design(1); } return null; } void choose(AI& ai, const Design@ dsg, bool randomizeName=true) { set(dsg); @designer = null; findSize = false; string baseName = dsg.name; if(customName.length != 0) { baseName = customName; } else if(randomizeName) { if(dsg.hasTag(ST_IsSupport)) baseName = autoSupportNames[randomi(0,autoSupportNames.length-1)]; else baseName = autoFlagNames[randomi(0,autoFlagNames.length-1)]; } string name = baseName; uint try = 0; while(ai.empire.getDesign(name) !is null) { name = baseName + " "; appendRoman(++try, name); } if(name != dsg.name) dsg.rename(name); //Set design settings/support behavior if(purpose == DP_Support) { if(dsg.total(SV_SupportSupplyCapacity) > 0.01) { DesignSettings settings; settings.behavior = SG_Brawler; dsg.setSettings(settings); } else if(dsg.totalHP > 50 * dsg.size) { DesignSettings settings; settings.behavior = SG_Shield; dsg.setSettings(settings); } else { DesignSettings settings; settings.behavior = SG_Cannon; dsg.setSettings(settings); } } ai.empire.addDesign(ai.empire.getDesignClass("AI", true), dsg); if(cast(ai.designs).log) ai.print("Chose design for purpose "+uint(purpose)+" at size "+dsg.size); } void step(AI& ai, Designs& designs) { if(active is null) { if(designer is null) { if(designs.race is null || !designs.race.preCompose(this)) prepare(ai); if(designs.race !is null && designs.race.postCompose(this)) return; } if(potential.length >= ai.behavior.designEvaluateCount) { //Find the best design out of all our potentials const Design@ best; double bestScore = 0.0; for(uint i = 0, cnt = potential.length; i < cnt; ++i) { double w = scores[i]; if(w > bestScore) { bestScore = w; @best = potential[i]; } } potential.length = 0; scores.length = 0; if(best !is null) choose(ai, best); } else if(designer !is null && active is null) { //Add a new design onto the list to be evaluated const Design@ dsg = design(ai, designs); if(dsg !is null && !dsg.hasFatalErrors()) { potential.insertLast(dsg); scores.insertLast(evaluate(ai, dsg)); /*if(designs.log)*/ /* ai.print("Designed for purpose "+uint(purpose)+" at size "+dsg.size+", weight "+evaluate(ai, dsg));*/ } } } else { set(active.mostUpdated()); } } void set(const Design@ dsg) { if(active is dsg) return; @active = dsg; targetBuildCost = dsg.total(HV_BuildCost); targetMaintenance = dsg.total(HV_MaintainCost); targetLaborCost = dsg.total(HV_LaborCost); targetSize = dsg.size; dps = dsg.total(SV_DPS); hp = dsg.totalHP + dsg.total(SV_ShieldCapacity); supplyDrain = dsg.total(SV_SupplyDrain); } }; const Design@ scaleDesign(const Design@ orig, int newSize) { DesignDescriptor desc; resizeDesign(orig, newSize, desc); return makeDesign(desc); } final class Designs : AIComponent { RaceDesigns@ race; int nextTargetId = 0; array designing; array completed; array automatic; void create() { @race = cast(ai.race); } void start() { //Design some basic support sizes design(DP_Support, 1); design(DP_Support, 2); design(DP_Support, 4); design(DP_Support, 8); design(DP_Support, 16); } void save(SaveFile& file) { file << nextTargetId; uint cnt = designing.length; file << cnt; for(uint i = 0; i < cnt; ++i) { saveDesign(file, designing[i]); designing[i].save(this, file); } cnt = automatic.length; file << cnt; for(uint i = 0; i < cnt; ++i) { saveDesign(file, automatic[i]); if(!isDesigning(automatic[i])) { file.write1(); automatic[i].save(this, file); } else { file.write0(); } } cnt = completed.length; file << cnt; for(uint i = 0; i < cnt; ++i) { saveDesign(file, completed[i]); if(!isDesigning(completed[i])) { file.write1(); completed[i].save(this, file); } else { file.write0(); } } } bool isDesigning(DesignTarget@ targ) { for(uint i = 0, cnt = designing.length; i < cnt; ++i) { if(designing[i] is targ) return true; } return false; } void load(SaveFile& file) { file >> nextTargetId; uint cnt = 0; file >> cnt; for(uint i = 0; i < cnt; ++i) { auto@ targ = loadDesign(file); targ.load(this, file); designing.insertLast(targ); } file >> cnt; for(uint i = 0; i < cnt; ++i) { auto@ targ = loadDesign(file); if(file.readBit()) targ.load(this, file); automatic.insertLast(targ); } file >> cnt; for(uint i = 0; i < cnt; ++i) { auto@ targ = loadDesign(file); if(file.readBit()) targ.load(this, file); completed.insertLast(targ); } } array loadIds; DesignTarget@ loadDesign(int id) { if(id == -1) return null; for(uint i = 0, cnt = loadIds.length; i < cnt; ++i) { if(loadIds[i].id == id) return loadIds[i]; } DesignTarget data; data.id = id; loadIds.insertLast(data); return data; } DesignTarget@ loadDesign(SaveFile& file) { int id = -1; file >> id; if(id == -1) return null; else return loadDesign(id); } void saveDesign(SaveFile& file, DesignTarget@ data) { int id = -1; if(data !is null) id = data.id; file << id; } void postLoad(AI& ai) { loadIds.length = 0; } const Design@ get_currentSupport() { for(int i = automatic.length - 1; i >= 0; --i) { if(automatic[i].purpose == DP_Support && automatic[i].active !is null) return automatic[i].active; } return null; } void getSupportAverages(double& hp, double& dps, double& supDrain) { hp = 0; dps = 0; supDrain = 0; uint count = 0; for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { auto@ targ = automatic[i]; if(targ.purpose != DP_Support) continue; if(targ.active is null) continue; hp += targ.hp / double(targ.targetSize); dps += targ.dps / double(targ.targetSize); supDrain += targ.supplyDrain / double(targ.targetSize); count += 1; } if(count == 0) { hp = 40.0; dps = 0.30; supDrain = 1.0; } else { hp /= double(count); dps /= double(count); supDrain /= double(count); } } DesignPurpose classify(Object@ obj) { if(obj is null || !obj.isShip) return DP_Combat; Ship@ ship = cast(obj); return classify(ship.blueprint.design); } DesignPurpose classify(const Design@ dsg, DesignPurpose defaultPurpose = DP_Combat) { if(dsg is null) return defaultPurpose; for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { if(automatic[i].contains(dsg)) return DesignPurpose(automatic[i].purpose); } for(uint i = 0, cnt = completed.length; i < cnt; ++i) { if(completed[i].contains(dsg)) return DesignPurpose(completed[i].purpose); } if(dsg.hasTag(ST_Mothership)) return DP_Mothership; if(dsg.hasTag(ST_Gate)) return DP_Gate; if(dsg.hasTag(ST_Slipstream)) return DP_Slipstream; if(dsg.hasTag(ST_Support)) return DP_Support; if(dsg.hasTag(ST_Station)) return DP_Defense; double dps = dsg.total(SV_DPS); if(dsg.total(SV_MiningRate) > 0) return DP_Miner; if(dsg.size == 16.0 && dsg.total(SV_DPS) < 2.0) return DP_Scout; if(dps > 0.1 * dsg.size || dsg.total(SV_SupportCapacity) > 0) return DP_Combat; return defaultPurpose; } DesignTarget@ design(uint purpose, int size, int targetCost = 0, int targetMaint = 0, double targetLabor = 0, bool findSize = false) { for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { auto@ target = automatic[i]; if(target.purpose != purpose) continue; if(target.targetSize != size) continue; if(targetCost != 0 && target.targetBuildCost > targetCost) continue; if(targetMaint != 0 && target.targetMaintenance > targetMaint) continue; if(targetLabor != 0 && target.targetLaborCost > targetLabor) continue; if(target.findSize != findSize) continue; return target; } DesignTarget targ(purpose, size); targ.findSize = findSize; targ.targetBuildCost = targetCost; targ.targetMaintenance = targetMaint; targ.targetLaborCost = targetLabor; automatic.insertLast(targ); return design(targ); } DesignTarget@ design(DesignTarget@ target) { target.id = nextTargetId++; designing.insertLast(target); return target; } DesignTarget@ get(const Design@ dsg) { for(uint i = 0, cnt = automatic.length; i < cnt; ++i) { if(automatic[i].contains(dsg)) return automatic[i]; } return null; } DesignTarget@ scale(const Design@ dsg, int newSize) { if(dsg.newer !is null) { auto@ newTarg = get(dsg.newest()); if(newTarg.targetSize == newSize) return newTarg; @dsg = dsg.newest(); } DesignTarget@ previous = get(dsg); uint purpose = DP_Combat; if(previous !is null) purpose = previous.purpose; else purpose = classify(dsg); DesignTarget target(purpose, newSize); target.id = nextTargetId++; @target.active = scaleDesign(dsg, newSize); ai.empire.changeDesign(dsg, target.active, ai.empire.getDesignClass(dsg.cls.name, true)); if(previous !is null) automatic.remove(previous); automatic.insertLast(target); return target; } uint chkInd = 0; void tick(double time) { if(designing.length != 0) { //chkInd = (chkInd+1) % designing.length; // Getting 1 design first is better than getting all of them later chkInd = 0; auto@ target = designing[chkInd]; target.step(ai, this); if(target.active !is null) { designing.removeAt(chkInd); if(automatic.find(target) == -1) completed.insertLast(target); } } } }; AIComponent@ createDesigns() { return Designs(); }