Files
2018-07-17 14:15:37 +02:00

903 lines
22 KiB
ActionScript

import hooks;
import influence;
import resources;
import influence_global;
from ai.diplomacy import CardAI, VoteAI, AIDiplomacy, VoteState;
const float RESERVE_MIN = 0.33f;
const int MONEY_MIN_PER_SUPP = 30;
const int MONEY_MAX_PER_SUPP = 200;
const int MONEY_FAIR_PER_SUPP = 60;
const int ENERGY_MIN_PER_SUPP = 50;
const int ENERGY_MAX_PER_SUPP = 200;
const int ENERGY_FAIR_PER_SUPP = 80;
const double SHIP_FAIR_PER_SUPP = 20.0;
const int PL_FAIR_PER_RESLV = 5;
const int ARTIFACT_FAIR = 1;
enum VoteStance {
S_Neutral,
S_GivenUp,
S_For,
S_Against,
};
final class VoteData : Savable, VoteState {
uint voteId = 0;
double forWeight = 1.0;
double againstWeight = 1.0;
double importance = 1.0;
uint stance = S_Neutral;
bool isRace = false;
int ourOffer = -1;
int pursueOffer = -1;
bool prompted = false;
InfluenceVote vote;
bool get_takenStance() {
return stance == S_For || stance == S_Against;
}
bool get_side() {
if(stance == S_Neutral && prompted)
return forWeight >= againstWeight;
return stance == S_For;
}
void save(SaveFile& file) {
file << voteId;
file << stance;
file << ourOffer;
file << pursueOffer;
file << prompted;
}
void load(SaveFile& file) {
file >> voteId;
file >> stance;
file >> ourOffer;
file >> pursueOffer;
file >> prompted;
}
InfluenceVote@ get_data() {
return vote;
}
bool get_isRaceVote() {
return isRace;
}
void set_isRaceVote(bool value) {
isRace = value;
}
double get_priority() {
return importance;
}
void set_priority(double value) {
importance = value;
}
double get_weightOppose() {
return againstWeight;
}
void set_weightOppose(double value) {
againstWeight = value;
}
double get_weightSupport() {
return forWeight;
}
void set_weightSupport(double value) {
forWeight = value;
}
};
class DiplomacyAI : AIDiplomacy {
bool log = false;
float reservePct;
int influenceCap;
int freeInfluence;
int reservedInfluence;
array<InfluenceCard> cards;
array<VoteData@> votes;
int totalSupport = 0;
int totalSupportCost = 0;
uint prevVotes = uint(-1);
void load(SaveFile& file) {
uint cnt = 0;
file >> cnt;
votes.length = cnt;
for(uint i = 0; i < cnt; ++i) {
@votes[i] = VoteData();
file >> votes[i];
}
file >> prevVotes;
}
void save(SaveFile& file) {
uint cnt = votes.length;
file << cnt;
for(uint i = 0; i < cnt; ++i)
file << votes[i];
file << prevVotes;
}
array<InfluenceCard>& get_cardList() {
return cards;
}
uint get_selfMask() {
Empire@ emp = empire;
uint mask = 0;
mask |= emp.mask;
if(emp.SubjugatedBy !is null) {
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
Empire@ check = getEmpire(i);
if(check is emp.SubjugatedBy || check.SubjugatedBy is emp.SubjugatedBy)
mask |= check.mask;
}
}
return mask;
}
uint enemiesMask(int maxStanding = -20) {
uint mask = 0;
for(uint i = 0, cnt = getEmpireCount(); i < cnt; ++i) {
Empire@ other = getEmpire(i);
if(other is empire)
continue;
if(empire.isHostile(other) || getStanding(other) < maxStanding)
mask |= other.mask;
}
return mask;
}
void summarize() {
//Calculate new reservation percentage
reservePct = RESERVE_MIN;
//Calculate actual available influence
influenceCap = empire.InfluenceCap;
int totalInfluence = empire.Influence;
reservedInfluence = min(totalInfluence, int(float(influenceCap) * reservePct));
freeInfluence = totalInfluence - reservedInfluence;
//Sync up our held cards
cards.syncFrom(empire.getInfluenceCards());
for(uint i = 0, cnt = cards.length; i < cnt; ++i) {
auto@ card = cards[i];
if(card.type.cls == ICC_Support) {
totalSupport += card.getWeight();
totalSupportCost += card.getPlayCost();
}
}
}
void buyCards() {
//See if we can buy something
if(freeInfluence <= 0)
return;
auto@ cardStack = getInfluenceStack();
double totalWeight = 0.0;
const StackInfluenceCard@ buyCard;
for(uint i = 0, cnt = cardStack.length; i < cnt; ++i) {
double w = getBuyWeight(cardStack[i]);
if(w <= 0)
continue;
totalWeight += w;
if(randomd() < w / totalWeight)
@buyCard = cardStack[i];
}
if(buyCard !is null) {
if(log)
print("Buy "+buyCard.formatTitle());
buyCardFromInfluenceStack(empire, buyCard.id);
}
}
Targets targs;
array<any> considerData;
array<any> playData;
void considerActions() {
if(freeInfluence <= 0)
return;
double totalWeight = 0.0;
InfluenceCard@ playCard;
considerData.length = 10;
for(uint i = 0, cnt = cards.length; i < cnt; ++i) {
double w = getPlayWeight(cards[i], considerData);
if(w <= 0)
continue;
totalWeight += w;
if(randomd() < w / totalWeight) {
@playCard = cards[i];
playData = considerData;
}
}
if(playCard !is null) {
if(log)
print("Play "+playCard.formatTitle());
play(playCard, playData);
}
}
void considerVotes() {
//Sync up new votes
uint newVotes = getLastInfluenceVoteId();
if(newVotes != prevVotes) {
for(uint i = prevVotes+1; i <= newVotes; ++i) {
VoteData state;
state.voteId = i;
syncInfluenceVote(state.voteId, state.vote);
considerVote(state);
votes.insertLast(state);
}
prevVotes = newVotes;
}
//See if we should vote on something
double totalWeight = 0.0;
VoteData@ voteOn;
for(uint i = 0, cnt = votes.length; i < cnt; ++i) {
double w = votes[i].importance;
if(votes[i].prompted)
w = (w+1.0) * 3.0;
if(w == 0)
continue;
totalWeight += w;
if(randomd() < w / totalWeight)
@voteOn = votes[i];
}
if(voteOn !is null) {
//Reconsider this vote
syncInfluenceVote(voteOn.voteId, voteOn.vote);
if(!voteOn.vote.active) {
votes.remove(voteOn);
return;
}
else {
considerVote(voteOn);
if(!voteOn.takenStance && !voteOn.prompted)
return;
}
//Don't vote if the vote is already going our way
if(!voteOn.prompted) {
if(voteOn.side) {
if((double(voteOn.vote.totalFor) / double(voteOn.vote.totalAgainst+1)) > randomd(1.0, 1.15)
&& (!voteOn.isRace || voteOn.vote.highestContributor is empire))
return;
}
else {
if((double(voteOn.vote.totalAgainst) / double(voteOn.vote.totalFor)) > randomd(1.0, 1.15)
&& (!voteOn.isRace || voteOn.vote.lowestContributor is empire))
return;
}
}
//See if we can play some cards into it
double totalWeight = 0.0;
InfluenceCard@ playCard;
considerData.length = 10;
for(uint i = 0, cnt = cards.length; i < cnt; ++i) {
double w = getPlayWeight(voteOn, cards[i], considerData);
if(w <= 0)
continue;
totalWeight += w;
if(randomd() < w / totalWeight) {
@playCard = cards[i];
playData = considerData;
}
}
bool voted = false;
if(playCard !is null) {
if(log)
print("Play into vote "+voteOn.vote.formatTitle()+": "+playCard.formatTitle());
voted = vote(voteOn, playCard, playData);
if(voted)
voteOn.prompted = false;
}
if(!voted) {
//See if we should make an offer
if(voteOn.pursueOffer == -1 && voteOn.ourOffer == -1 && !voteOn.isRace && !voteOn.prompted)
makeOffer(voteOn);
}
}
}
void considerVote(VoteData& state) {
auto@ vote = state.vote;
//Reconsider whether to support or oppose this vote
state.forWeight = 1.0;
state.againstWeight = 1.0;
state.importance = 1.0;
state.isRace = false;
int standing = getStanding(vote.startedBy);
//Check if our offers are valid
bool pursueValid = false, ourValid = false;
for(uint i = 0, cnt = state.vote.offers.length; i < cnt; ++i) {
if(state.vote.offers[i].fromEmpire is empire) {
state.ourOffer = state.vote.offers[i].id;
ourValid = true;
}
if(state.vote.offers[i].id == state.pursueOffer)
pursueValid = true;
}
if(!ourValid && state.ourOffer != -1) {
state.ourOffer = -1;
}
if(!pursueValid && state.pursueOffer != -1) {
state.pursueOffer = -1;
state.stance = S_Neutral;
}
//Check if we got prompted
if(!state.prompted) {
//TODO
/*if(ai.callOuts.contains(state.voteId)) {*/
/* state.prompted = true;*/
/* ai.callOuts.erase(state.voteId);*/
/*}*/
}
//Our votes are good
if(vote.startedBy is empire)
state.forWeight *= 1000.0;
//We don't like people we don't like
if(empire.hostileMask & vote.startedBy.mask != 0)
state.againstWeight *= 2.0;
if(standing < -20 && vote.startedBy.valid)
state.againstWeight *= 2.0;
//We like allies
if(vote.startedBy.mask & allyMask != 0 || standing > 50) {
if(standing > 75)
state.forWeight *= 4.0;
else
state.forWeight *= 2.0;
}
//Check the vote's hooks
for(uint i = 0, cnt = vote.type.ai.length; i < cnt; ++i) {
VoteAI@ hook = cast<VoteAI>(vote.type.ai[i]);
if(hook !is null)
hook.consider(this, state, vote);
}
//Adapt our stance accordingly
if(state.pursueOffer == -1) {
if(state.stance == S_Neutral) {
if(state.forWeight > state.againstWeight) {
if(state.importance >= 3.0 || state.forWeight > state.againstWeight * randomd(1.3, 2.2)) {
if(state.importance >= 10.0 || canPass(state))
state.stance = S_For;
}
}
else {
if(state.importance >= 3.0 || state.againstWeight > randomd(1.3, 2.2)) {
bool canWin = true;
if(state.importance >= 10.0 || canFail(state))
state.stance = S_Against;
}
}
}
else if(state.stance == S_For) {
if(state.againstWeight > state.forWeight * 10.0)
state.stance = S_Against;
else if(state.importance < 5.0 && !canPass(state, margin=2.0))
state.stance = S_Neutral;
}
else if(state.stance == S_Against) {
if(state.forWeight > state.againstWeight * 10.0)
state.stance = S_For;
else if(state.importance < 5.0 && !canFail(state, margin=2.0))
state.stance = S_Neutral;
}
}
//Check if we should pursue an offer
if(state.vote.offers.length != 0 && state.pursueOffer == -1) {
const InfluenceVoteOffer@ bestOffer;
double bestRatio = 0.0;
//Calculate the minimum fairity ratio we want
bestRatio = 0.9;
if(freeInfluence + reservedInfluence > influenceCap)
bestRatio *= pow(0.95, double(freeInfluence + reservedInfluence - influenceCap) / 2.0);
else
bestRatio *= pow(1.05, double(influenceCap - freeInfluence - reservedInfluence));
for(uint i = 0, cnt = state.vote.offers.length; i < cnt; ++i) {
auto@ offer = state.vote.offers[i];
if(offer.fromEmpire is empire)
continue;
if(offer.fromEmpire.isHostile(empire))
continue;
if(state.stance != S_Neutral) {
if(offer.side != (state.stance == S_For))
continue;
}
double fair = 0;
for(uint i = 0, cnt = offer.offers.length; i < cnt; ++i) {
auto@ thing = offer.offers[i];
switch(thing.type) {
case DOT_Money:
fair += thing.value / double(MONEY_FAIR_PER_SUPP);
break;
case DOT_Energy:
fair += thing.value / double(ENERGY_FAIR_PER_SUPP);
break;
case DOT_Card:
fair += offer.fromEmpire.getCostOfCard(thing.id) * thing.value;
break;
case DOT_Fleet: {
Ship@ ship = cast<Ship>(thing.obj);
if(ship !is null) {
auto@ dsg = ship.blueprint.design;
if(dsg !is null)
fair += dsg.size / SHIP_FAIR_PER_SUPP;
}
} break;
case DOT_Planet: {
Planet@ pl = cast<Planet>(thing.obj);
if(pl !is null)
fair += (pl.primaryResourceLevel+1) * PL_FAIR_PER_RESLV;
} break;
case DOT_Artifact:
fair += ARTIFACT_FAIR;
break;
}
}
double offerRatio = fair / double(offer.support);
//Offerer-specific ratio mods
if(allyMask & offer.fromEmpire.mask != 0) {
offerRatio *= 1.1;
}
else {
int standing = getStanding(offer.fromEmpire);
if(standing > 50)
offerRatio *= 1.1;
else if(standing < -10)
offerRatio *= 0.8;
}
if(offerRatio > bestRatio) {
bestRatio = offerRatio;
@bestOffer = offer;
}
}
if(bestOffer !is null) {
state.pursueOffer = bestOffer.id;
state.stance = bestOffer.side ? S_For : S_Against;
if(log)
print("pursue offer from "+bestOffer.fromEmpire.name+" on vote "+state.vote.formatTitle());
}
}
}
void checkOffer(VoteData& state) {
if(state.pursueOffer == -1)
return;
//Probably faster than syncing and checking
claimInfluenceVoteOffer_server(empire, state.voteId, state.pursueOffer);
}
double getVoteGap(VoteData@ state) {
double gap = 0.0;
if(state.side) {
if(!state.isRace) {
gap = state.vote.totalAgainst - state.vote.totalFor;
}
else {
Empire@ bestEmp = state.vote.highestContributor;
if(bestEmp !is null)
gap = state.vote.getVoteFrom(bestEmp) - state.vote.getVoteFrom(empire);
}
}
else {
if(!state.isRace) {
gap = state.vote.totalFor - state.vote.totalAgainst;
}
else {
Empire@ bestEmp = state.vote.lowestContributor;
if(bestEmp !is null)
gap = (-state.vote.getVoteFrom(bestEmp)) - (-state.vote.getVoteFrom(empire));
}
}
return gap;
}
bool canPass(VoteData@ state, double margin = 1.0) {
//Check whether we can realistically pass this vote
double gap = getVoteGap(state);
if(gap < 0)
return true;
double availInfluence = freeInfluence;
if(state.importance >= 3.0)
availInfluence += reservedInfluence;
double availSupport = min(availInfluence * (double(totalSupport) / double(totalSupportCost)), double(totalSupport));
if(availSupport < gap * pow(0.5, margin))
return false;
return true;
}
bool canFail(VoteData@ state, double margin = 1.0) {
//Check whether we can realistically fail this vote
double gap = getVoteGap(state);
if(gap < 0)
return true;
double availInfluence = freeInfluence;
if(state.importance >= 3.0)
availInfluence += reservedInfluence;
double availSupport = min(availInfluence * (double(totalSupport) / double(totalSupportCost)), double(totalSupport));
if(availSupport < gap * pow(0.5, margin))
return false;
return true;
}
void makeOffer(VoteData@ state) {
int needSupp = getVoteGap(state);
if(needSupp <= 0)
return;
InfluenceVoteOffer off;
off.side = state.side;
off.support = 0;
int offerMoney = 0;
int offerEnergy = 0;
for(uint i = 0, cnt = randomi(1,3); i < cnt; ++i) {
int supp = ceil(double(needSupp) * randomd(0.5, 1.0));
if(supp == 0)
continue;
switch(randomi(0,2)) {
case 0: {
int curMoney = double(empire.RemainingBudget) * 0.8;
if(curMoney / supp < MONEY_MIN_PER_SUPP)
break;
int perSupp = randomi(MONEY_MIN_PER_SUPP, min(MONEY_MAX_PER_SUPP, curMoney / supp));
offerMoney += supp * perSupp;
needSupp -= supp;
off.support += supp;
} break;
case 1: {
int curEnergy = double(empire.EnergyStored) * 0.8;
if(curEnergy / supp < ENERGY_MIN_PER_SUPP)
break;
int perSupp = randomi(ENERGY_MIN_PER_SUPP, min(ENERGY_MAX_PER_SUPP, curEnergy / supp));
offerEnergy += supp * perSupp;
needSupp -= supp;
off.support += supp;
} break;
case 2: {
if(cards.length == 0)
break;
auto@ card = cards[randomi(0, cards.length-1)];
int cost = card.getPurchaseCost(empire);
if(cost != 0 && cost != INDETERMINATE) {
if(cost > supp * 1.5)
break;
DiplomacyOffer offCard;
offCard.type = DOT_Card;
offCard.id = card.id;
offCard.value = 1;
off.offers.insertLast(offCard);
needSupp -= cost;
off.support += cost;
}
} break;
}
}
if(offerMoney != 0) {
DiplomacyOffer money;
money.type = DOT_Money;
money.value = offerMoney;
off.offers.insertLast(money);
}
if(offerEnergy != 0) {
DiplomacyOffer energy;
energy.type = DOT_Energy;
energy.value = offerEnergy;
off.offers.insertLast(energy);
}
if(off.offers.length != 0 && off.support != 0) {
makeInfluenceVoteOffer_server(empire, state.voteId, off);
if(log)
print("make offer on vote "+state.vote.formatTitle());
}
}
double getBuyWeight(const StackInfluenceCard@ card) {
if(card.type.ai.length == 0)
return 0.0;
if(!card.canPurchase(empire))
return 0.0;
int cost = card.getPurchaseCost(empire);
if(cost > freeInfluence || cost == 0)
return 0.0;
double w = 1.0;
//Weight based on buy cost
w *= 1.0 - (double(cost) / double(freeInfluence+1));
//Only sometimes buy leader only cards
if(card.type.leaderOnly && getSenateLeader() !is empire)
w *= 0.05;
//Extra weight based on card support
if(card.type.cls == ICC_Support) {
int supp = card.getWeight();
if(supp != 0 && supp != INDETERMINATE)
w *= sqrt(double(supp * card.uses));
}
//Extra weight based on larger stacks
w *= sqrt(double(card.uses));
//Weight on AI hooks
for(uint i = 0, cnt = card.type.ai.length; i < cnt; ++i) {
auto@ hook = cast<CardAI>(card.type.ai[i]);
if(hook !is null)
hook.considerBuy(this, card, w);
}
return w;
}
double getPlayWeight(const InfluenceCard@ card, array<any>& data) {
if(card.type.ai.length == 0)
return 0.0;
if(!card.canPlay(null))
return 0.0;
double w = 1.0;
int cost = card.getPlayCost();
if(cost != INDETERMINATE) {
if(cost > freeInfluence)
return 0.0;
//Initial weight based on play cost
w *= 1.0 - (double(cost) / double(freeInfluence+1));
}
//Weight on AI hooks
uint hookCnt = card.type.ai.length;
if(hookCnt > data.length)
data.length = hookCnt;
targs.set(card.targets);
card.targetDefaults(targs);
for(uint i = 0; i < hookCnt; ++i) {
auto@ hook = cast<CardAI>(card.type.ai[i]);
if(hook !is null)
hook.considerAct(this, card, data[i], targs, w);
}
return w;
}
void play(InfluenceCard@ card, array<any>& data) {
uint hookCnt = card.type.ai.length;
targs.set(card.targets);
card.targetDefaults(targs);
bool canPlay = true;
for(uint i = 0; i < hookCnt; ++i) {
auto@ hook = cast<CardAI>(card.type.ai[i]);
if(hook !is null) {
if(!hook.act(this, card, data[i], targs)) {
canPlay = false;
break;
}
}
}
if(canPlay && card.canPlay(targs))
playInfluenceCard_server(empire, card.id, targs);
else if(log)
print(" failed to play "+card.formatTitle()+" - "+canPlay);
}
double getPlayWeight(VoteData@ state, const InfluenceCard@ card, array<any>& data) {
if(card.type.ai.length == 0)
return 0.0;
if(!card.canPlay(state.vote, null))
return 0.0;
//Ignore cards we shouldn't play into this anyway
if(card.type.sideMode == ICS_Support && !state.side)
return 0.0;
if(card.type.sideMode == ICS_Oppose && state.side)
return 0.0;
int availInfluence = freeInfluence;
if(state.importance >= 3.0)
availInfluence += reservedInfluence;
double w = 1.0;
int cost = card.getPlayCost(state.vote);
if(cost != INDETERMINATE) {
if(cost > availInfluence)
return 0.0;
//Initial weight based on play cost
w *= 1.0 - (double(cost) / double(availInfluence+1));
}
int weight = card.getWeight(state.vote);
if(weight == 0)
return 0.0;
if(weight != INDETERMINATE)
w *= weight;
//Weight on AI hooks
uint hookCnt = card.type.ai.length;
if(hookCnt > data.length)
data.length = hookCnt;
targs.set(card.targets);
card.targetDefaults(targs);
for(uint i = 0; i < hookCnt; ++i) {
auto@ hook = cast<CardAI>(card.type.ai[i]);
if(hook !is null)
hook.considerVote(this, state, card, data[i], targs, w);
}
return w;
}
bool vote(VoteData@ state, InfluenceCard@ card, array<any>& data) {
uint hookCnt = card.type.ai.length;
targs.set(card.targets);
card.targetDefaults(targs);
bool canPlay = true;
for(uint i = 0; i < hookCnt; ++i) {
auto@ hook = cast<CardAI>(card.type.ai[i]);
if(hook !is null) {
if(!hook.vote(this, state, card, data[i], targs)) {
canPlay = false;
break;
}
}
}
if(canPlay && card.canPlay(state.vote, targs)) {
playInfluenceCard_server(empire, card.id, targs, state.vote.id);
if(state.pursueOffer != -1)
checkOffer(state);
return true;
}
else {
if(log)
print(" failed to play "+card.formatTitle()+" - "+canPlay);
return false;
}
}
//AI IMPLEMENTATIONS
Empire@ get_empire() {
return null;
}
uint get_allyMask() {
return 0;
}
int getStanding(Empire@ emp) {
return 0;
}
void print(const string& str) {
}
Object@ considerOwnedPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerImportantPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerOwnedSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerImportantSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerDefendingSystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerDefendingPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerEnemySystems(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerFleets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerEnemyFleets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerEnemyPlanets(const CardAI& hook, Targets& targets, VoteState@ vote = null, const InfluenceCard@ card = null) {
return null;
}
Object@ considerMatchingImportRequests(const CardAI& hook, Targets& targets, VoteState@ vote, const InfluenceCard@ card, const ResourceType@ type, bool considerExisting) {
return null;
}
void markProtecting(Object@ obj) {
}
};