mirror of
https://github.com/wesnoth/wesnoth
synced 2025-04-26 15:33:16 +00:00
551 lines
24 KiB
Python
551 lines
24 KiB
Python
#!UNSAFE_WPY
|
|
|
|
# Sample implementation - converting to the wail library rather than
|
|
# wesnoth directly. This show how easily bot writers can convert old
|
|
# bots, which maintained namespace, to the wail.
|
|
# Wail requires Python 2.5+. If the following statement fails, you likely
|
|
# are not running Python 2.5.x or later.
|
|
from __future__ import with_statement
|
|
|
|
import time
|
|
import wail, random
|
|
|
|
## Copyright 2006 by Michael Schmahl
|
|
## This code is available under the latest version of the GNU Public License.
|
|
## See COPYING for details. Some inspiration and code derived from "sample.py"
|
|
## by allefant.
|
|
##
|
|
## This is my attempt at a 'chess-like' AI. All moves are motivated by
|
|
## an underlying evaluation function. The actual eval function doesn't
|
|
## need to be coded, because moves can be scored and ranked based on the
|
|
## incremental change in the evaluation. Unlike a chess-playing program,
|
|
## though, this program does no lookahead, because the branching factor
|
|
## is prohibitively high (potentially in the thousands), and because then
|
|
## the script would have to create an internal model of the game state.
|
|
##
|
|
## Despite the lack of any lookahead, I still consider this AI to be
|
|
## chess-like because it evaluates every possible move and attack, even
|
|
## those that are obviously (to a human) bad. How can a computer know
|
|
## that these are bad moves unless it actually checks?
|
|
##
|
|
## The evaluation function is:
|
|
##
|
|
## (1) side_score = village_score
|
|
## + sum(unit_score, over all units)
|
|
## + positional_score
|
|
##
|
|
## The value of a unit can be highly subjective, but to simplify, assume
|
|
## that any level-1 unit is just as valuable as any other level-1 unit.
|
|
## Specifically, the value of a unit will be:
|
|
##
|
|
## (2) unit_score = (1 + level + %xp)(1 + %hp)
|
|
##
|
|
## Leaders are be considered three levels higher than their actual level.
|
|
## So a freshly-recruited level-1 unit is worth 4.0 points. And a level-2
|
|
## unit with half its hitpoints remaining, but halfway to level 3, is
|
|
## worth 6.75 points.
|
|
##
|
|
## One question is: How much is a village worth, compared to a (typical)
|
|
## unit? A typical unit is worth 15 to 20 gold, because that is how much
|
|
## we paid for it. A village is worth two or three gold *per turn* as
|
|
## long as it is held. (The village is worth three gold when it offsets
|
|
## a unit's upkeep.) So we must make some assumptions as to the value of
|
|
## a present gold piece, compared to a future gold piece. Assume a decay
|
|
## rate of 1.5 (i.e. a gold piece one turn from now is worth two-thirds
|
|
## of a gold piece now). This makes the present value of a village equal
|
|
## to twice its income. If we set the value of a typical unit at 16 gold,
|
|
## we get that an upkeep-offsetting village is worth 1.5 points, and a
|
|
## supernumerary village is worth 1.0 points. For simplicity, the value
|
|
## of each village is set at 1.0.
|
|
##
|
|
## (3) village_score = number of villages
|
|
##
|
|
## The positional score is the most interesting term of equation (1),
|
|
## because that, more than anything else, will guide the AI's behavior.
|
|
##
|
|
## First, we want the AI to expand to capture villages. So, for each unit,
|
|
## it is scored based on how far it is from the nearest unowned or enemy
|
|
## village. If the distance is zero, the unit has actually captured the
|
|
## village, so in that limit, the value should be equal to the village
|
|
## value. As the distance approaces infinity, the score should tend
|
|
## toward zero. This suggests something like:
|
|
##
|
|
## (4) village_proximity = c / (c + distance)
|
|
##
|
|
## I have selected c to be equal to equal to the unit's movement. This
|
|
## means that (approximately) a unit one turn away from capturing a village
|
|
## gets 0.5 points; two turns, 0.33 points, etc. Although an exponential
|
|
## relationship would be more accurate, exponentiation is expensive, and
|
|
## better avoided, since thousands of moves are evaluated per turn.
|
|
##
|
|
## Second, we want units to stand on defensive terrain when within range
|
|
## of the enemy. The 'right' way to do this would be to count up all the
|
|
## potential attackers at the destination square, see how much damage they
|
|
## might do, and score the move based on how much damage would be dealt/
|
|
## prevented. Again, this is much too slow. I have found a reasonable
|
|
## approximation is:
|
|
##
|
|
## (5) exposure_penalty = -defense_modifier / 10
|
|
##
|
|
## Maybe much too simple, but easy to calculate! In future editions, perhaps
|
|
## I should take into account how damaged the unit is, or at least make some
|
|
## attempt to count the attackers.
|
|
##
|
|
## Third, we want units to heal when damaged or poisoned. Referring to
|
|
## equation (2), we can see that the value of healing is:
|
|
##
|
|
## (6) healing_score = healing / max_hitpoints * (1 + level + %xp)
|
|
##
|
|
## We consider poison, which does 8 damage *per turn*, to be equivalent to
|
|
## 16 points of actual damage, for the same reason a village's real value is
|
|
## twice its income (see above).
|
|
##
|
|
## Fourth, we want units to guard villages if the enemy is in range to take
|
|
## them. If, by stationing a unit on a village, we prevent the enemy from
|
|
## taking it, we have prevented a 2-point swing in the enemy's favor. Again
|
|
## considering a decay rate of 2/3 per turn, this means the garrison value
|
|
## is 4/3. But since there is no guarantee that our garrison will be
|
|
## successful (perhaps the enemy will take the village anyway; perhaps it is
|
|
## not possible to garrison all threatened villages), we will cut this in half.
|
|
##
|
|
## (7) garrison_score = 2/3
|
|
##
|
|
## Fifth, we want our leader to stay near a keep. Otherwise, any gold we
|
|
## might have accumulated will be wasted. And finally, we want units to move
|
|
## toward the enemy leader. These are accomplished by treating keeps as
|
|
## if they were unowned villages (for our leader), and the enemy leader
|
|
## as if it were a village (for everyone else).
|
|
##
|
|
## This should be all that is required to play a decent game of Wesnoth.
|
|
## This AI scores quite well against the Wesnoth default AI, which may be
|
|
## surprising, because it uses no sophisticated tools. There is no attempt
|
|
## to use any of the path-finding tools made available by the API (which
|
|
## would be too slow to be used thousands of times every turn). There is
|
|
## no attempt to use combination attacks (meaning, that even though none of
|
|
## several units can favorably attack a certain target, if they all attack
|
|
## in the same turn, the result is likely to be favorable). No attempt is
|
|
## made to assign units individually to targets.
|
|
##
|
|
## Some bad behaviors may result from these shortcomings:
|
|
##
|
|
## If the map is maze-like, or simply has a few corners surrounded by
|
|
## impassable terrain, units may get stuck. On Cynsaun Battlefield, for
|
|
## example, a group of units got stuck in the middle of the river, trying
|
|
## to capture a village on the other side of the deep-water hexes.
|
|
##
|
|
## An enemy unit may get completely surrounded by friendly units, who are
|
|
## weak in comparison to the enemy, and our AI will make no attempt to kill
|
|
## the enemy unit. (Think six Wolf Riders surrounding an Orcish Grunt.)
|
|
## Usually one or more of these units will find something else to do, allowing
|
|
## a few Archers to take their place and start to wear down the Grunt. Or
|
|
## the Grunt will attack, getting damaged in the process, and creating a
|
|
## chance-to-kill for one of the Wolves.
|
|
##
|
|
## If there is an unoccupied village in a corner of the map, our AI will
|
|
## send every unit that is closer to the village than any other, to that
|
|
## village. Often, only one unit is necessary. Thus, harassing villages
|
|
## with scouts may be a much more viable strategy against this AI than
|
|
## against human players, or against the default AI.
|
|
##
|
|
## For those interested in results, I have set up a tournament between my
|
|
## AI and the default AI. The tournament consists of one match on each of
|
|
## the mainline two-player maps (except Wesbowl, naturally). In each map,
|
|
## each opponent is allowed to be player 1 once. If there is no decision
|
|
## after two games, two more games are played, repeating as necessary until
|
|
## one opponent has won the match. All games are played with a 50-turn
|
|
## limit, 2 gold per village, 70% experience, and no fog. (I think there
|
|
## is a bug (feature?) that AIs ignore fog, so I disabled it to improve the
|
|
## observer's (my) experience.) Factions are chosen randomly.
|
|
##
|
|
## Map W-L-D %Win Match result
|
|
## Blitz 2-0-0 100 Win
|
|
## Caves of the Basilisk 4-2-0 67 Win
|
|
## Charge 3-1-0 75 Win
|
|
## Cynsaun Battlefield (1gpv) 2-0-0 100 Win
|
|
## Den of Onis 4-2-0 67 Win
|
|
## Hamlets 2-0-0 100 Win
|
|
## Hornshark Island 0-2-0 0 Loss
|
|
## Meteor Lake 2-0-0 100 Win
|
|
## Sablestone Delta 2-0-0 100 Win
|
|
## Silverhead Crossing 3-1-0 75 Win
|
|
## Sulla's Ruins 2-0-0 100 Win
|
|
## ** Overall 25-8-0 76 10 Wins, 1 Loss (91%)
|
|
|
|
# UNIT SCORE MODIFIERS
|
|
|
|
BASE_UNIT_SCORE = 1 # Base worth of a unit
|
|
LEVEL_SCORE = 1 # Worth/level
|
|
LEADER_SCORE = 3 # Leader worth
|
|
FULL_XP_SCORE = 1 # How much is partial XP worth (1 is 100% XP = 1 pt)
|
|
|
|
# This score is then multiplied by a factor dependant on the price of the unit
|
|
# this makes expensive units worth more to the AI
|
|
|
|
COST_SCORE = 0 #
|
|
BASE_COST_SCORE = 1 #
|
|
|
|
# Formula:
|
|
# Base_Score = BASE_UNIT_SCORE + level * LEVEL_SCORE + is_leader * LEADER_SCORE + xp/max_xp * FULL_XP_SCORE
|
|
# Cost_Modifier = BASE_COST_SCORE + price * COST_SCORE
|
|
# Unit_Score(unit_k) = Base_Score * Cost_Modifier
|
|
|
|
# POSITION SCORE MODIFIERS
|
|
|
|
NO_MOVE_PEN = 0 # Penalty for not moving (doesn't quite work)
|
|
NEXT_TO_ENEMY_PEN = 0 # Penalty for moving next to an enemy and not attacking
|
|
STAND_NEXT_TO_ENEMY_PEN = 0 # Penalty for standing next to an enemy without moving or attacking
|
|
|
|
# MISC SCORE MODIFIERS
|
|
|
|
LEVEL_CHANCE_BONUS = 0 # How much a level-up is worth
|
|
|
|
VILLAGE_SCORE = 1 # How much capturing a village is worth
|
|
ENEMY_VILLAGE_BONUS = 1 # How much extra is an enemy village worth
|
|
|
|
GARRISON_SCORE = 2.0/3 # How much defending a village is worth
|
|
DEFENSE_FACTOR = 1.0/1000 # How much to penalize a unit for being in an attackable position
|
|
|
|
HEAL_FACTOR = 1 # How much is healing worth
|
|
HEAL_ATTACKABLE = .5 # How much relative to healing is healing when attackable worth
|
|
HEAL_POISON = 16 # How much is healing from poison worth
|
|
|
|
HP_SCALE = .1 # Unit HP/turn (for recruitment)
|
|
|
|
def pos(p):
|
|
if p==None: return ("Nowhere")
|
|
return ("(%s,%s)"%(p.x+1,p.y+1))
|
|
|
|
class AI:
|
|
def __init__(self):
|
|
self.get_villages()
|
|
self.get_keeps()
|
|
self.mapsize = max((wail.get_map().x,wail.get_map().y)) / 30.0
|
|
self.stats = [0,0]
|
|
|
|
def report_stats(self):
|
|
wail.log_message("%d moves, %d fights evaluated" % (self.stats[0],self.stats[1]))
|
|
|
|
def get_villages(self):
|
|
self.notmyvillages = []
|
|
m = wail.get_map()
|
|
for x in range(m.x):
|
|
for y in range(m.y):
|
|
loc = wail.get_location(x,y)
|
|
if m.is_village(loc):
|
|
for team in wail.get_teams():
|
|
if team.owns_village(loc) and not team.is_enemy:
|
|
break
|
|
else:
|
|
self.notmyvillages.append(loc)
|
|
|
|
def get_keeps(self):
|
|
self.keeps = []
|
|
m = wail.get_map()
|
|
for x in range(m.x):
|
|
for y in range(m.y):
|
|
loc = wail.get_location(x,y)
|
|
if m.is_keep(loc):
|
|
# If the enemy is occupying the keep, it is "off-limits" to our leader.
|
|
# Otherwise, if our leader has strayed too far, it might attempt to go
|
|
# to the enemy keep, which basically means we lose.
|
|
if loc not in wail.get_enemy_destinations_by_unit().keys():
|
|
self.keeps.append(loc)
|
|
|
|
def recruit(self):
|
|
# I haven't discussed this at all. Perhaps a few paragraphs would be in order.
|
|
if wail.get_current_team().gold < 16: return
|
|
|
|
# find our leader
|
|
leaderpos = None
|
|
for location,unit in wail.get_units().iteritems():
|
|
if unit.can_recruit and unit.side == wail.get_current_team().side:
|
|
leaderpos = location
|
|
break
|
|
|
|
# no leader? can't recruit
|
|
if leaderpos == None: return
|
|
|
|
# is our leader on a keep? If not, move to a keep
|
|
# Maybe should always go to nearest keep
|
|
if not leaderpos in self.keeps:
|
|
for dest in wail.get_destinations_by_unit().get(leaderpos,[]):
|
|
if dest in self.keeps:
|
|
leaderpos = wail.move_unit(leaderpos,dest)
|
|
break
|
|
|
|
# is our leader on a keep now? If not, can't recruit
|
|
if leaderpos not in self.keeps: return
|
|
|
|
# build up a list of recruits and scores for each
|
|
recruit_list = []
|
|
sumweights = 0
|
|
for recruit in wail.get_current_team().recruits():
|
|
weight = self.recruit_score(recruit)
|
|
if weight < 0.01: weight = 0.01
|
|
recruit_list.append((recruit.name,weight))
|
|
sumweights += weight
|
|
|
|
# repeatedly recruit until we fail
|
|
while 1:
|
|
|
|
# pick a random recruit in proportion to the weights
|
|
r = random.uniform(0,sumweights)
|
|
for recruit,weight in recruit_list:
|
|
r -= weight
|
|
if r < 0: break
|
|
|
|
# just use leaderpos for the location; wesnoth will always
|
|
# recruit on the nearest adjacent tile
|
|
if not wail.recruit_unit(recruit,leaderpos): break
|
|
|
|
def map_score(self,recruit):
|
|
# calculate average speed in hexes/turn
|
|
# and average defense in effective hp
|
|
m = wail.get_map()
|
|
n = m.x * m.y
|
|
|
|
speed = 0.0
|
|
defense = 0.0
|
|
for x in range(m.x):
|
|
for y in range(m.y):
|
|
loc = wail.get_location(x,y)
|
|
speed += 1.0 / recruit.movement_cost(loc)
|
|
rdm = recruit.defense_modifier(loc) - 1
|
|
if rdm:
|
|
defense += 100.0 / rdm
|
|
|
|
else:
|
|
defense += 1.00
|
|
|
|
# speed is more important on larger maps
|
|
speed *= self.mapsize * recruit.movement / n
|
|
|
|
# scaled down because effective hp is over the lifetime of the unit,
|
|
# while other scores are based on per-turn quantities
|
|
defense *= HP_SCALE * recruit.hitpoints / n
|
|
return speed,defense
|
|
|
|
def combat_score(self,recruit):
|
|
# combat advantage, in hp/turn, averaged over all enemy units
|
|
tot = 0.0
|
|
n = 0
|
|
for loc,enem in wail.get_units().iteritems():
|
|
if not enem.is_enemy: continue
|
|
n += 1
|
|
tot += self.combat_advantage(recruit,enem)
|
|
tot -= self.combat_advantage(enem,recruit)
|
|
|
|
return tot/n
|
|
|
|
def combat_advantage(self,attacker,defender):
|
|
# combat advantage for attacker attacking defender
|
|
best = 0.0
|
|
for weapon in attacker.attacks():
|
|
damage = weapon.damage * weapon.num_attacks * defender.damage_from(weapon) / 100.0
|
|
|
|
best_retal = 0.0
|
|
for retaliation in defender.attacks():
|
|
if weapon.range == retaliation.range:
|
|
retal = retaliation.damage * retaliation.num_attacks * attacker.damage_from(retaliation) / 100.0
|
|
if retal > best_retal: best_retal = retal
|
|
|
|
damage -= best_retal
|
|
if damage > best: best = damage
|
|
|
|
# scale down because not every attack hits
|
|
return best/2
|
|
|
|
def recruit_score(self,recruit):
|
|
speed,defense = self.map_score(recruit)
|
|
combat = self.combat_score(recruit)
|
|
rval = (speed + defense + combat)/recruit.cost
|
|
# only report "interesting" results
|
|
if rval > 0:
|
|
wail.log_message("%s: (%.2f + %.2f + %.2f) / %d = %.3f" % (recruit.name,speed,defense,combat,recruit.cost,rval))
|
|
return rval
|
|
|
|
def do_one_move(self):
|
|
enemlocs = wail.get_enemy_destinations_by_unit().keys()
|
|
self.enemdests = wail.get_enemy_units_by_destination().keys()
|
|
bestmove = (0,None,None,None) # score,orig,dest,target
|
|
|
|
# find the best move
|
|
for orig in wail.get_destinations_by_unit().keys():
|
|
# get a baseline score for this unit "standing pat"
|
|
base_score = self.eval_move(orig,orig)
|
|
for dest in wail.get_destinations_by_unit()[orig]:
|
|
# Bug workaround -- if we have recruited this turn,
|
|
# get_destinations_by_unit() is incorrect
|
|
if dest in wail.get_units().keys() and dest != orig: continue
|
|
score = self.eval_move(orig,dest) - base_score
|
|
if score > bestmove[0]:
|
|
bestmove = (score,orig,dest,dest)
|
|
for target in wail.get_adjacent_tiles(dest):
|
|
if target in enemlocs:
|
|
fight = self.eval_fight(wail.get_units()[orig],dest,target)+score
|
|
if orig == dest:
|
|
fight += STAND_NEXT_TO_ENEMY_PEN + NO_MOVE_PEN
|
|
else:
|
|
fight += NEXT_TO_ENEMY_PEN
|
|
if fight > bestmove[0]:
|
|
bestmove = (fight,orig,dest,target)
|
|
|
|
if bestmove[1] == None:
|
|
# no move improved the position, therefore we are done
|
|
return False
|
|
|
|
score,orig,dest,target = bestmove
|
|
wail.log_message("%.3f: %s->%s@%s"%(score,pos(orig),pos(dest),pos(target)))
|
|
if dest != orig: wail.move_unit(orig,dest)
|
|
if dest in self.notmyvillages: self.notmyvillages.remove(dest)
|
|
if target != dest: wail.attack_unit(dest,target)
|
|
|
|
return True
|
|
|
|
def eval_fight(self,unit,dest,target):
|
|
self.stats[1] += 1
|
|
enem = wail.get_units().get(target,None)
|
|
if not enem: return 0
|
|
|
|
# the base value for each unit:
|
|
# I should give more weight to defeating a garrison
|
|
unit_k = (LEVEL_SCORE*unit.type().level + BASE_UNIT_SCORE + LEADER_SCORE*unit.can_recruit\
|
|
+ FULL_XP_SCORE * unit.experience * 1.0 / unit.max_experience) * (BASE_COST_SCORE + unit.type().cost * COST_SCORE)
|
|
enem_k = (LEVEL_SCORE*enem.type().level + BASE_UNIT_SCORE + LEADER_SCORE*enem.can_recruit\
|
|
+ FULL_XP_SCORE * enem.experience * 1.0 / enem.max_experience) * (BASE_COST_SCORE + enem.type().cost * COST_SCORE)
|
|
|
|
unit_hp,enem_hp = unit.attack_statistics(dest,target)
|
|
score = 0.0
|
|
for hp,p in enem_hp.iteritems():
|
|
score += p * (enem.hitpoints - hp) * enem_k / enem.max_hitpoints
|
|
if hp<=0: score += p * enem_k
|
|
for hp,p in unit_hp.iteritems():
|
|
score -= p * (unit.hitpoints - hp) * unit_k / unit.max_hitpoints
|
|
if hp<=0: score -= p * unit_k
|
|
|
|
enem_xp = 8*enem.type().level
|
|
if enem.type().level == 0:
|
|
enem_xp = 4
|
|
unit_xp = 8*unit.type().level
|
|
if unit.type().level == 0:
|
|
unit_xp = 4
|
|
|
|
if enem.type().level >= unit.max_experience - unit.experience:
|
|
for hp, p in unit_hp.iteritems():
|
|
if hp > 0: score += LEVEL_CHANCE_BONUS * p * unit_k
|
|
elif enem_xp >= unit.max_experience - unit.experience:
|
|
for hp, p in enem_hp.iteritems():
|
|
if hp <= 0: score += LEVEL_CHANCE_BONUS * p * unit_k
|
|
if unit.type().level >= enem.max_experience - enem.experience:
|
|
for hp, p in enem_hp.iteritems():
|
|
if hp > 0: score -= LEVEL_CHANCE_BONUS * p * enem_k
|
|
elif unit_xp >= enem.max_experience - enem.experience:
|
|
for hp, p in unit_hp.iteritems():
|
|
if hp <= 0: score += LEVEL_CHANCE_BONUS * p * enem_k
|
|
|
|
return score
|
|
|
|
def eval_move(self,orig,dest):
|
|
enemlocs = wail.get_enemy_destinations_by_unit().keys()
|
|
self.stats[0] += 1
|
|
score = 0.0
|
|
|
|
unit = wail.get_units().get(orig,None)
|
|
if not unit: return
|
|
unit_k = (LEVEL_SCORE*unit.type().level + BASE_UNIT_SCORE + LEADER_SCORE*unit.can_recruit\
|
|
+ FULL_XP_SCORE * unit.experience * 1.0 / unit.max_experience) * (BASE_COST_SCORE + unit.type().cost * COST_SCORE)
|
|
|
|
# subtract 1 because terrain might be a factor
|
|
speed = unit.type().movement - 1
|
|
|
|
attackable=False
|
|
if dest in self.enemdests:
|
|
attackable = True
|
|
else:
|
|
for adj in wail.get_adjacent_tiles(dest):
|
|
if adj in self.enemdests:
|
|
attackable = True
|
|
break
|
|
|
|
# capture villages
|
|
if dest in self.notmyvillages:
|
|
score += VILLAGE_SCORE
|
|
for team in wail.get_teams():
|
|
if team.owns_village(dest) and team.is_enemy:
|
|
score += ENEMY_VILLAGE_BONUS
|
|
|
|
bestdist=100
|
|
if unit.can_recruit:
|
|
# leader stays near keep
|
|
for keep in self.keeps:
|
|
dist=dest.distance_to(keep)
|
|
if dist<bestdist:
|
|
bestdist=dist
|
|
if dist<=1: break
|
|
else:
|
|
# everyone else moves toward enemy leader
|
|
for loc,enem in wail.get_units().iteritems():
|
|
if enem.is_enemy and enem.can_recruit:
|
|
dist=dest.distance_to(loc)
|
|
if dist<bestdist:
|
|
bestdist=dist
|
|
if dist<=1: break
|
|
if bestdist > 1:
|
|
for vil in self.notmyvillages:
|
|
if dest==vil: continue
|
|
dist=dest.distance_to(vil)
|
|
if dist<bestdist:
|
|
bestdist=dist
|
|
if dist<=1: break
|
|
score += (1.0 * speed) / (bestdist + speed)
|
|
|
|
# healing
|
|
# I am ignoring the value of healers, and regenerating units. I don't think unit abilities
|
|
# are correctly reported by the API, anyway.
|
|
if (unit.poisoned or unit.hitpoints<unit.max_hitpoints) and wail.get_map().is_village(dest):
|
|
if unit.poisoned: healing = HEAL_POISON
|
|
else:
|
|
healing = unit.max_hitpoints-unit.hitpoints
|
|
if healing > 8: healing = 8
|
|
# reduce the healing bonus if we might get killed first
|
|
if attackable: healing *= HEAL_ATTACKABLE
|
|
score += HEAL_FACTOR * healing * unit_k / unit.max_hitpoints
|
|
|
|
if attackable:
|
|
# defense
|
|
score -= unit.defense_modifier(dest) * DEFENSE_FACTOR
|
|
|
|
# garrison
|
|
if wail.get_map().is_village(dest): score += GARRISON_SCORE
|
|
|
|
# reduce chances of standing next to a unit without attacking for a whole turn
|
|
if dest == orig:
|
|
score -= NO_MOVE_PEN
|
|
for target in wail.get_adjacent_tiles(dest):
|
|
if target in enemlocs:
|
|
score -= STAND_NEXT_TO_ENEMY_PEN
|
|
break
|
|
else:
|
|
for target in wail.get_adjacent_tiles(dest):
|
|
if target in enemlocs:
|
|
score -= NEXT_TO_ENEMY_PEN
|
|
break
|
|
|
|
# end mod
|
|
|
|
return score
|
|
|
|
|
|
st = time.time()
|
|
ai = AI()
|
|
ai.recruit()
|
|
while True:
|
|
if not ai.do_one_move():
|
|
break
|
|
ai.recruit()
|
|
ai.report_stats()
|
|
|
|
print "======================="
|
|
print "bruteforce ran for %0.4f-seconds." % (time.time() - st)
|
|
print "======================="
|