Skip to content

Instantly share code, notes, and snippets.

@dound
Created September 4, 2012 08:46
Show Gist options
  • Save dound/3618664 to your computer and use it in GitHub Desktop.
Save dound/3618664 to your computer and use it in GitHub Desktop.
Compute optimal mix of cottages, barracks, and troops to train to maximize might (or combat power). View stats computed with this script here: bit.ly/UoSdYS
#/usr/bin/env python
"""Computes optimal mix for maximum might or combat power gain."""
from scipy import optimize
# number of places you can build in each city
SLOTS_PER_CITY = 31
# T1, T2, T3 (combat units)
TIER_BASE_TRAINING_TIME = (90, 480, 1200)
TIER_POP_USED = (2, 4, 8)
TIER_RESOURCE_USED = (500, 1000, 2000)
TIER_MIGHT = (4, 16, 24)
TIER_POWER = (1, 2, 4)
# Population from cottages (with 0% tax)
COTTAGE_POP = (0, 50, 300, 600, 1000, 1500, 2100, 2800, 3600, 4500, 5500)
def idle_pop_per_day(cottage_levels):
"""5% of total pop regenerates every 6 minutes (so 100% every 2hr)"""
return sum(COTTAGE_POP[lvl] for lvl in cottage_levels) * 12
def train_barracks_factor(num_barracks, barracks_lvl):
"""Barracks impact factor on training time"""
return num_barracks + ((num_barracks * barracks_lvl - num_barracks) / 10.0)
def train_nonbarracks_factor(geometry, training_building, knight):
"""Non-barracks impact factor on training time"""
return 1 + (geometry + training_building) / 10.0 + knight / 200.0
def train_factor(num_barracks, barracks_level,
geometry, training_building, knight):
"""Factor used in computing training time"""
bf = train_barracks_factor(num_barracks, barracks_level)
nbf = train_nonbarracks_factor(geometry, training_building, knight)
return bf * nbf
def time_to_train(tier_num, troop_quantity, tf):
"""Returns time to train troops.
tier_num - tier number of the troops (1, 2, or 3)
troop_quantity - number of troop to train
tf - training factor (see train_factor)
"""
base_sec = TIER_BASE_TRAINING_TIME[tier_num - 1] * troop_quantity
return base_sec / tf
def troop_trained_in_given_time(tier_num, sec_to_train, tf):
"""Returns troop count which can be trained in a given time (sec)."""
return tf * sec_to_train / TIER_BASE_TRAINING_TIME[tier_num - 1]
def optimal_troop_mix(maximize_might,
percent_usable_idle_pop_per_day,
knight_level,
cottage_level,
barracks_level,
geometry_level,
training_building_level,
reserved_slots,
resource_limit=None,
min_t3=0,
training_sec=86400,
poison_level=0,
potion_level=0,
display=True):
"""Computes optimal cottage, barracks, and troop mix for maximum might or
combat power gain for a single *city* in a given time period.
Returns a dictionary with keys cottages, barracks, t1, t2, and t3 which
correspond to how many of each of these the player should have / train
daily.
maximize_might - True if might should be maximized, otherwise (defensive)
combat power will be maximized.
percent_usable_idle_pop_per_day - percentage [0.0, 1.0] indicating how much
of the maximum idle population which can be regenerated per day is usable.
If you play at least every 2 hours, this is 1.0. If you are gone for 8
hours straight each day, then 6 hours of regenerated population is lost
(you're already maxed out after 2 hours) so this parameter would be passed
as (24.-6)/24 or 0.75.
knight_level - the level of your knight which is set to Marshall at the
beginning of all training initiations.
cottage_level - the level of ALL of your cottages (e.g., 9 if all of your
cottages are level 9).
barracks_level - the level of ALL of your barracks.
geometry_level - the level of your geometry research.
training_building_level - the level of ALL your training buildings
(blacksmith for T1 and heavy cavalry, stable for swordsmen and cavalry, and
workshop for rams and catapults). (e.g., 9 if each of these buildings is
level 9).
reserved_slots - number of plots of land reserved in your first city for
buildings OTHER THAN your barracks and cottages. Do not count your castle.
Recommendation:
City 1: 6 - workshop, blacksmith, rally point, embassy, stable, watchtower
City 2: 4 - workshop, blacksmith, rally point, watchtower
(plus one each for labs until research is done)
resource_limit - total number of resources (food, wood, stone, and ore
combined) which are available.
min_t3 - minimum number of T3 to train. May be useful if you need T3 for
attacking, etc. (T3 aren't the best at anything by any measure except
power per troop [which is only useful when attacking since attacks have
limited size]).
training_sec - number of seconds available for training. Defaults to 1 day.
poison_level - level of poison research. Only used if maximizing power.
potion_level - level of health research. Only used if maximizing power.
display - whether to print to the screen or not
"""
# adjust tier powers based on knight and research levels
knight_mult = knight_level / 200.0
power_multiplier = 1 + knight_mult + (poison_level + potion_level) / 20.0
tier_power = tuple(map(lambda x: x * power_multiplier, TIER_POWER))
# The number of cottages and barracks are in a pretty small range, so for
# simplicity we'll just brute force every cottage/barrack combination.
# Also, cottages and barracks have to be integers so this ensures they are
# integers too. (Troop counts also have to be integers, but rounding those
# down isn't a big deal since they're large numbers anyway).
free_slots = SLOTS_PER_CITY - reserved_slots
best_score = best_res = best_idx = None
output = []
for num_cottages in xrange(1, 10):
num_barracks = free_slots - num_cottages
idle_pop = idle_pop_per_day([cottage_level] * num_cottages)
usable_idle_pop = idle_pop * percent_usable_idle_pop_per_day
tf = train_factor(num_barracks, barracks_level, geometry_level,
training_building_level, knight_level)
tier_training_sec = map(lambda t: time_to_train(t, 1, tf), [1, 2, 3])
score, troops = _optimal_troop_mix(maximize_might,
resource_limit,
min_t3,
training_sec,
usable_idle_pop,
tier_training_sec,
tier_power)
plan = _str_build_plan(troops, training_sec, usable_idle_pop,
tier_training_sec, tier_power)
res_fmt = '%d Cottages, %2d Barracks => %s'
res = res_fmt % (num_cottages, num_barracks, plan)
if display:
output.append(res)
if best_score is None or score > best_score:
best_idx = len(output) - 1
best_score = score
best_res = res
if display == 'html':
print '<div style="font-size:10pt;font-family:courier">'
style = '<b style="background-color:#CFC">'
output[best_idx] = style + output[best_idx] + '</b>'
print '<br/>'.join(output)
print '</div>'
elif display:
output[best_idx] += ' <-- BEST'
print '\n'.join(output)
return best_res
def sumprod(a, b):
"""Returns the sum of the product of each pair of elements in a and b."""
assert len(a) == len(b)
return sum(a[i] * b[i] for i in xrange(len(a)))
def _str_build_plan(troops, training_sec, usable_idle_pop, tier_training_time,
tier_power):
"""Returns a string describing a troop build plan"""
mght = sumprod(TIER_MIGHT, troops) / 1e3
pop = sumprod(TIER_POP_USED, troops)
rsrc = sumprod(TIER_RESOURCE_USED, troops) / 1e6
power = sumprod(tier_power, troops) / 1e3
time_sec = sumprod(tier_training_time, troops)
time_percent = 100.0 * time_sec / training_sec
pop_percent = 100.0 * pop / usable_idle_pop
fmt = 'T1:%6d T2:%5d T3:%5d in %2.1fhr (%2.1f%% used) w/%4.1fmil ' \
'total rsrc & %dk pop used (%3.1f%%) -> %4dk might, %4dk power'
return fmt % (troops[0], troops[1], troops[2],
time_sec / 3600.0, time_percent,
rsrc, pop / 1e3, pop_percent, mght, power)
def _optimal_troop_mix(maximize_might, resource_limit, min_t3, training_sec,
usable_idle_pop, tier_training_sec, tier_power):
"""Returns a 2-tuple of (maximal value, tuple of troop amounts per tier)"""
# Variables:
# 0) T1 - number of Tier 1 troops to train
# 1) T2 - number of Tier 2 troops to train
# 2) T3 - number of Tier 3 troops to train
# function to maximize
if maximize_might:
f = might
f_deriv = TIER_MIGHT
else:
f = lambda t1, t2, t3: combat_power(t1, t2, t3, tier_power)
f_deriv = tier_power
# scipy's optimizer finds the minimal value, so we multiply might by -1
# (so big mights are now better than smaller mights to the minimizer)
f_to_min = lambda x: -1.0 * f(x[0], x[1], x[2])
f_deriv = map(lambda v: -1.0 * v, f_deriv)
f_jac = lambda x: f_deriv
# Constraints
constraints = []
def c(function, const_jac=None, jac=None):
"""Helper to add constraints"""
if const_jac:
jac = lambda x: const_jac
constraints.append(dict(type='ineq', fun=function, jac=jac))
# Cannot have negative troops
c(lambda x: x[0], [1.0, 0.0, 0.0])
c(lambda x: x[1], [0.0, 1.0, 0.0])
c(lambda x: x[2], [0.0, 0.0, 1.0])
# Cannot use more idle population than available
c(lambda x: usable_idle_pop - sumprod(x, TIER_POP_USED),
jac=lambda x: [-TIER_POP_USED[i] for i in xrange(3)])
# Cannot use more build time than we were given
c(lambda x: training_sec - sumprod(x, tier_training_sec),
jac=lambda x: [-tier_training_sec[i] for i in xrange(3)])
# Must build at least the requested number of T3 (if any)
c(lambda x: x[2] - min_t3,
jac=lambda x: [0.0, 0.0, 1.0])
# Cannot use more resources than available
if resource_limit is not None:
c(lambda x: resource_limit - sumprod(x, TIER_RESOURCE_USED),
jac=lambda x: [-TIER_RESOURCE_USED[i] for i in xrange(3)])
res = optimize.minimize(f_to_min, [1e6, 1e6, 1e6], jac=f_jac,
constraints=constraints, method='SLSQP')
if not res.success:
err = 'Failed to solve might=%s rsrc=%s time=%s pop=%s train=%s: %s'
raise Exception(err % (maximize_might, resource_limit, training_sec,
usable_idle_pop, tier_training_sec,
res.message))
return -res.fun, tuple(int(v) for v in res.x)
def might(t1, t2, t3):
"""Computes how much might a mix of T1, T2, and T3 troops provide."""
return TIER_MIGHT[0] * t1 + TIER_MIGHT[1] * t2 + TIER_MIGHT[2] * t3
def combat_power(t1, t2, t3, tier_power):
"""Computes how much combat power troops provide."""
return tier_power[0] * t1 + tier_power[1] * t2 + tier_power[2] * t3
def main():
"""Compute an optimal troop mix for some specific inputs."""
# Level 212 knight, miss 6 hours of regen pop per day, level 9 everything,
# and 7 reserved slots for non-cottage non-barracks buildings. Plug in
# your own numbers.
x = optimal_troop_mix(True,
percent_usable_idle_pop_per_day=0.75, # some downtime
knight_level=212,
cottage_level=9,
barracks_level=9,
geometry_level=9,
training_building_level=9,
resource_limit=None, # farm, farm, farm!
min_t3=0, # these aren't best at anything (except concentrated
# firepower) so if you want them you must ask for them
reserved_slots=7) # about right for troop city still researching
print 'BEST:\n', x
def make_summary_doc():
"""Generates the contents of the summary doc."""
print '''
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8"/>
<link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/combo?3.1.0/build/cssreset/reset.css&amp;3.1.0/build/cssfonts/fonts.css&amp;3.1.0/build/cssbase/base.css"/>
</head><body>'''
h1_fmt = '<h1>Usable Idle Population = %2.0f%%</h1>'
for in_game_time in (1.0, 0.75, 0.5, 0.25):
print h1_fmt % (100.0 * in_game_time,)
for knight in (212, 50):
for lvl in range(9, 11):
fmt = '<h2>Knight Level %d, Building / Research Level %d</h2>'
print fmt % (knight, lvl)
for r in range(4, 9):
print '<h3>%d Reserved Plots</h3><p>' % r
optimal_troop_mix(True, in_game_time, knight, lvl, lvl,
lvl, lvl, reserved_slots=r,
display='html')
print '</p>'
print '</body></html>'
if __name__ == '__main__':
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment