Created
May 27, 2013 14:39
-
-
Save Muon/5657395 to your computer and use it in GitHub Desktop.
Analysis of Achron's units using Lanchester's square law
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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