Skip to content

Instantly share code, notes, and snippets.

@Muon
Created May 27, 2013 14:39
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Muon/5657395 to your computer and use it in GitHub Desktop.
Save Muon/5657395 to your computer and use it in GitHub Desktop.
Analysis of Achron's units using Lanchester's square law
from __future__ import division
import csv
from collections import namedtuple
from math import ceil, sqrt
import sys
# General constants
TICKS_PER_SECOND = 18
RP_COST = 80
RP_LC_CYCLE_TIME = 216
RP_QP_CYCLE_TIME = 270
RP_YIELD_SIZE = 8
IMPORTER_COST = 50
IMPORTER_CYCLE_TIME = 852
IMPORTER_YIELD_SIZE = 1
# Primary constants
NUM_IMPORTERS = 2
NUM_RPS = 5
NUM_ATTACKERS = 5
# Parallelism factors
CESO_PARALLELISM = 1
VECGIR_VIR_PARALLELISM = 1
VECGIR_PULSER_PARALLELISM = 7
VECGIR_TERCHER_PARALLELISM = 5
VECGIR_HALCYON_PARALLELISM = 3
GREKIM_PARALLELISM = 6
# Compensate for CESO spending extra money on Importers by giving aliens RPs
# equal in LC value to CESO's Importers.
NUM_ALIEN_RPS = NUM_RPS + NUM_IMPORTERS * IMPORTER_COST / RP_COST
# http://stackoverflow.com/a/1695250/126977
def enum(*sequential, **named):
enums = dict(zip(sequential, range(len(sequential))), **named)
return type('Enum', (), enums)
MoveType = enum("GROUND", "AIR")
Unit = namedtuple("Unit", ["name", "lc", "qp", "reserves", "build_time", "faction", "hp", "ag_dps", "aa_dps", "ag_range", "aa_range", "move_type", "speed"])
def load_data(filename):
"""Load Achron unit data from CSV with JRC-style row headers."""
units = []
with open(filename, 'rt') as csvfile:
reader = csv.DictReader(csvfile)
for datum in reader:
name = datum["Unit Name"]
lc = int(datum["LC Cost"])
qp = int(datum["QP Cost"])
if datum["Faction"] == "CESO":
reserves = int(datum["Special Cost"])
else:
reserves = 0
build_time = round(float(datum["Build Time"]) * TICKS_PER_SECOND)
faction = datum["Faction"]
hp = int(datum["Health"])
ag_dps = float(datum["Avg damage/s Ground"])
aa_dps = float(datum["Avg damage/s Air"])
ag_range = int(datum["Attack Range Ground"])
aa_range = int(datum["Attack Range Air"])
speed = float(datum["Move Speed"])
if datum["Move type"] == "Ground":
move_type = MoveType.GROUND
elif datum["Move type"] == "Air":
move_type = MoveType.AIR
else:
assert False
unit = Unit(name, lc, qp, reserves, build_time, faction, hp, ag_dps, aa_dps, ag_range, aa_range, move_type, speed)
units.append(unit)
return units
def cost_gather_time(unit):
"""Calculate amount of time necessary to gather resources for unit."""
if unit.faction == "CESO":
dLCdt = (NUM_RPS * RP_YIELD_SIZE / RP_LC_CYCLE_TIME)
dQPdt = (NUM_RPS * RP_YIELD_SIZE / RP_QP_CYCLE_TIME)
dRdt = (NUM_IMPORTERS * IMPORTER_YIELD_SIZE / IMPORTER_CYCLE_TIME)
return ceil(max(unit.lc / dLCdt + unit.qp / dQPdt, unit.reserves / dRdt))
else:
dLCdt = (NUM_ALIEN_RPS * RP_YIELD_SIZE / RP_LC_CYCLE_TIME)
dQPdt = (NUM_ALIEN_RPS * RP_YIELD_SIZE / RP_QP_CYCLE_TIME)
return ceil(unit.lc / dLCdt + unit.qp / dQPdt)
def get_base_parallelism(unit):
"""Get maximum number of unit that can be built in parallel."""
if unit.faction == "CESO":
return CESO_PARALLELISM
elif unit.faction == "Grekim":
return GREKIM_PARALLELISM
elif unit.faction == "Vecgir":
if unit.name.endswith("Vir"):
return VECGIR_VIR_PARALLELISM
elif unit.name.endswith("Tercher"):
return VECGIR_TERCHER_PARALLELISM
elif unit.name.endswith("Halcyon"):
return VECGIR_HALCYON_PARALLELISM
return 1
def time_to_build_discrete(unit, n):
"""Calculate time to build n units by simulation (slow, reliable)."""
assert n >= 0
if n == 0:
return 0
G = cost_gather_time(unit)
P = get_base_parallelism(unit)
producers = [None] * P
B = unit.build_time
G_total = 0
count = 0
t = 0
in_flight = 0
while count < n:
if t > 0 and t % G == 0:
G_total += 1
for i, producer_start_time in enumerate(producers):
if producer_start_time is not None:
if t - producer_start_time >= B:
count += 1
in_flight -= 1
producers[i] = None
for i, producer_start_time in enumerate(producers):
if producers[i] is None and G_total > 0 and count + in_flight < n:
producers[i] = t
in_flight += 1
G_total -= 1
t += 1
return t
def time_to_build_continuous(unit, n):
"""Calculate time to build n units analytically (fast, untested)."""
G = cost_gather_time(unit)
P = get_base_parallelism(unit)
B = unit.build_time
parallelism_factor = min(P, ceil(B/G), n)
return G * parallelism_factor + (n - parallelism_factor) * max(G, B / P) + B
def time_to_build_old(unit, n):
"""Calculate time to build n units analytically (DEPRECATED)."""
G = cost_gather_time(unit)
P = get_base_parallelism(unit)
B = unit.build_time
return G + (n - 1) * max(G, B/P) + B
time_to_build = time_to_build_discrete
def get_relevant_dps(attacker, defender):
if defender.move_type == MoveType.GROUND:
return attacker.ag_dps
elif defender.move_type == MoveType.AIR:
return attacker.aa_dps
def get_relevant_range(attacker, defender):
if defender.move_type == MoveType.GROUND:
return attacker.ag_range
elif defender.move_type == MoveType.AIR:
return attacker.aa_range
def combat_effectiveness_in_range(attacker, defender):
"""Calculate combat effectiveness using Lanchester model when both attacker
and defender are in range of each other ."""
attacker_dps = get_relevant_dps(attacker, defender)
defender_dps = get_relevant_dps(defender, attacker)
attacker_effectiveness_sq = attacker.hp * attacker_dps
defender_effectiveness_sq = defender.hp * defender_dps
if attacker_effectiveness_sq > 0:
if defender_effectiveness_sq > 0:
return sqrt(attacker_effectiveness_sq / defender_effectiveness_sq)
else:
return float("inf")
else:
return 0.0
def range_compensation(attacker, defender):
delta_range = get_relevant_range(attacker, defender) - get_relevant_range(defender, attacker)
return max(get_relevant_dps(attacker, defender) * delta_range / defender.speed, 0) / defender.hp
def combat_effectiveness_out_of_range(attacker, defender):
"""Calculate combat effectiveness using Lanchester model when attacker
and defender are not necessarily in range of each other."""
alpha = combat_effectiveness_in_range(attacker, defender)
if alpha == float("inf") or alpha == 0:
return alpha
else:
# This was derived using Wolfram|Alpha.
return (range_compensation(attacker, defender) + alpha) / (range_compensation(defender, attacker) * alpha + 1)
def cost_effectiveness(attacker, defender, combat_model):
combat_effectiveness = combat_model(attacker, defender)
if combat_effectiveness == float("inf") or combat_effectiveness == 0:
return combat_effectiveness
else:
defender_build_time = time_to_build(defender, NUM_ATTACKERS * combat_effectiveness)
attacker_build_time = time_to_build(attacker, NUM_ATTACKERS)
# sys.stderr.write("Build %f %s to defend against %s (%fx), %ft vs %ft\n" % (NUM_ATTACKERS * combat_effectiveness, defender.name, attacker.name, combat_effectiveness, defender_build_time, attacker_build_time))
return defender_build_time / attacker_build_time
if __name__ == "__main__":
assert len(sys.argv) == 2, "Exactly one command line argument must be specified (the input file)"
units = load_data(sys.argv[1])
writer = csv.writer(sys.stdout)
writer.writerow(["Attacker\Defender"] + [defender.name for defender in units])
for attacker in units:
row_values = [attacker.name]
for defender in units:
# effectiveness = combat_effectiveness_out_of_range(attacker, defender)
effectiveness = cost_effectiveness(attacker, defender, combat_effectiveness_out_of_range)
row_values.append(effectiveness)
writer.writerow(row_values)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment