Remove Python AI support, addressing CVE-2009-0367
This commit is contained in:
parent
98b9afca18
commit
dd1d5064e3
16 changed files with 17 additions and 3472 deletions
|
@ -15,3 +15,14 @@ The release team should empty this file after each release.
|
|||
|
||||
Replays work again for single-player campaign scenarios.
|
||||
|
||||
***
|
||||
|
||||
This release contains an important security update, fixing a vulnerability
|
||||
that could allow third-party content (such as campaigns downloaded from the
|
||||
add-on server) to execute arbitrary code with user account privileges. Consult
|
||||
CVE-2009-0367 for details. All content currently on the official add-on server
|
||||
has been inspected to confirm that none of it exploits this vulnerability,
|
||||
and the add-on server itself has been patched to ensure that exploits can no
|
||||
longer be uploaded. Therefore, users of previous versions of Battle for Wesnoth
|
||||
who have received user-made content through the official add-on server and
|
||||
no other distribution channel need not fear that they have been compromised.
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
Version 1.4.7+svn:
|
||||
* campaigns:
|
||||
* Descent into Darkness:
|
||||
* Removed the custom python AI used in 'A Haunting in Winter'
|
||||
* language and i18n:
|
||||
* updated translations: French, Italian, Japanese
|
||||
* multiplayer:
|
||||
|
@ -9,6 +12,7 @@ Version 1.4.7+svn:
|
|||
* Additional screenmodes for when SDL can't guess them (patch #1108)
|
||||
* Fixed bug #12896: Map generator does not sync between clients when
|
||||
advancing in MP campaigns
|
||||
* Removed support for PythonAI to handle CVE-2009-0367
|
||||
|
||||
Version 1.4.7:
|
||||
* campaigns:
|
||||
|
|
24
configure.ac
24
configure.ac
|
@ -120,11 +120,6 @@ AC_ARG_ENABLE([static],
|
|||
[static=$enableval],
|
||||
[static=no])
|
||||
|
||||
AC_ARG_ENABLE([python],
|
||||
AS_HELP_STRING([--disable-python], [disable Python support]),
|
||||
[python=$enableval],
|
||||
[python=yes])
|
||||
|
||||
AC_ARG_ENABLE([python_install],
|
||||
AS_HELP_STRING([--enable-python-install], [enable installation of Python developer tools]),
|
||||
[python_install=$enableval],
|
||||
|
@ -309,12 +304,6 @@ AC_ARG_ENABLE([display-revision],
|
|||
[svnrev=no])
|
||||
|
||||
|
||||
if test "x$game" = "xno"
|
||||
then
|
||||
python=no
|
||||
AC_MSG_WARN([*** Game build disabled, suppressing Python support.])
|
||||
fi
|
||||
|
||||
if test "x$python" = "xno"
|
||||
then
|
||||
python_install=no
|
||||
|
@ -480,19 +469,6 @@ if test "x$python" = "xyes"; then
|
|||
if test "x$pythonfound" = "xyes"; then
|
||||
AC_MSG_RESULT(yes)
|
||||
|
||||
PYTHON_CFLAGS="-DHAVE_PYTHON -I$PYTHON_PREFIX/include/python$PYTHON_VERSION"
|
||||
|
||||
OLD_CPPFLAGS="$CPPFLAGS"
|
||||
OLD_CXXFLAGS="$CXXFLAGS"
|
||||
CPPFLAGS="$CPPFLAGS $PYTHON_CFLAGS"
|
||||
CXXFLAGS="$CXXFLAGS $PYTHON_CFLAGS"
|
||||
|
||||
AC_CHECK_HEADER([Python.h],
|
||||
[],
|
||||
[AC_MSG_WARN([*** Python include files not found! You should install Python development package. Python support disabled]); pythonfound=no])
|
||||
CPPFLAGS="$OLD_CPPFLAGS"
|
||||
CXXFLAGS="$OLD_CXXFLAGS"
|
||||
|
||||
if test "x$pythonfound" = "xyes"; then
|
||||
AC_SUBST([PYTHON_CFLAGS])
|
||||
|
||||
|
|
|
@ -1,531 +0,0 @@
|
|||
#!WPY
|
||||
|
||||
import wesnoth, 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((wesnoth.get_map().x,wesnoth.get_map().y)) / 30.0
|
||||
self.stats = [0,0]
|
||||
|
||||
def report_stats(self):
|
||||
wesnoth.log_message("%d moves, %d fights evaluated" % (self.stats[0],self.stats[1]))
|
||||
|
||||
def get_villages(self):
|
||||
self.notmyvillages = []
|
||||
m = wesnoth.get_map()
|
||||
for x in range(m.x):
|
||||
for y in range(m.y):
|
||||
loc = wesnoth.get_location(x,y)
|
||||
if m.is_village(loc):
|
||||
for team in wesnoth.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 = wesnoth.get_map()
|
||||
for x in range(m.x):
|
||||
for y in range(m.y):
|
||||
loc = wesnoth.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 wesnoth.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 wesnoth.get_current_team().gold < 16: return
|
||||
|
||||
# find our leader
|
||||
leaderpos = None
|
||||
for location,unit in wesnoth.get_units().iteritems():
|
||||
if unit.can_recruit and unit.side == wesnoth.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 wesnoth.get_destinations_by_unit().get(leaderpos,[]):
|
||||
if dest in self.keeps:
|
||||
leaderpos = wesnoth.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 wesnoth.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 wesnoth.recruit_unit(recruit,leaderpos): break
|
||||
|
||||
def map_score(self,recruit):
|
||||
# calculate average speed in hexes/turn
|
||||
# and average defense in effective hp
|
||||
m = wesnoth.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 = wesnoth.get_location(x,y)
|
||||
speed += 1.0 / recruit.movement_cost(loc)
|
||||
defense += 100.0 / recruit.defense_modifier(loc) - 1
|
||||
|
||||
# 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 wesnoth.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:
|
||||
wesnoth.log_message("%s: (%.2f + %.2f + %.2f) / %d = %.3f" % (recruit.name,speed,defense,combat,recruit.cost,rval))
|
||||
return rval
|
||||
|
||||
def do_one_move(self):
|
||||
enemlocs = wesnoth.get_enemy_destinations_by_unit().keys()
|
||||
self.enemdests = wesnoth.get_enemy_units_by_destination().keys()
|
||||
bestmove = (0,None,None,None) # score,orig,dest,target
|
||||
|
||||
# find the best move
|
||||
for orig in wesnoth.get_destinations_by_unit().keys():
|
||||
# get a baseline score for this unit "standing pat"
|
||||
base_score = self.eval_move(orig,orig)
|
||||
for dest in wesnoth.get_destinations_by_unit()[orig]:
|
||||
# Bug workaround -- if we have recruited this turn,
|
||||
# get_destinations_by_unit() is incorrect
|
||||
if dest in wesnoth.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 wesnoth.get_adjacent_tiles(dest):
|
||||
if target in enemlocs:
|
||||
fight = self.eval_fight(wesnoth.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
|
||||
wesnoth.log_message("%.3f: %s->%s@%s"%(score,pos(orig),pos(dest),pos(target)))
|
||||
if dest != orig: wesnoth.move_unit(orig,dest)
|
||||
if dest in self.notmyvillages: self.notmyvillages.remove(dest)
|
||||
if target != dest: wesnoth.attack_unit(dest,target)
|
||||
|
||||
return True
|
||||
|
||||
def eval_fight(self,unit,dest,target):
|
||||
self.stats[1] += 1
|
||||
enem = wesnoth.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 = wesnoth.get_enemy_destinations_by_unit().keys()
|
||||
self.stats[0] += 1
|
||||
score = 0.0
|
||||
|
||||
unit = wesnoth.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 wesnoth.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 wesnoth.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 wesnoth.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 wesnoth.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 wesnoth.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 wesnoth.get_adjacent_tiles(dest):
|
||||
if target in enemlocs:
|
||||
score -= STAND_NEXT_TO_ENEMY_PEN
|
||||
break
|
||||
else:
|
||||
for target in wesnoth.get_adjacent_tiles(dest):
|
||||
if target in enemlocs:
|
||||
score -= NEXT_TO_ENEMY_PEN
|
||||
break
|
||||
|
||||
# end mod
|
||||
|
||||
return score
|
||||
|
||||
ai = AI()
|
||||
ai.recruit()
|
||||
while 1:
|
||||
if not ai.do_one_move():
|
||||
break
|
||||
ai.recruit()
|
||||
ai.report_stats()
|
|
@ -1,91 +0,0 @@
|
|||
#!/usr/bin/python
|
||||
|
||||
# This is *not* a python AI, it's just run as AI so it can get access to
|
||||
# Python's runtime documentation. This documentation then simply is dumped to
|
||||
# stdout in a format ready to be pasted to the wiki.
|
||||
|
||||
def myhelp(topic, topics):
|
||||
"""Collect all the help topics into the given list."""
|
||||
doc = getattr(eval(topic), "__doc__")
|
||||
subtopics = []
|
||||
for subtopic in getattr(eval(topic), "__dict__", []):
|
||||
if subtopic.startswith("_"): continue
|
||||
myhelp(topic + "." + subtopic, subtopics)
|
||||
tc = getattr(eval(topic), "__class__", None)
|
||||
tt = getattr(tc, "__name__", None)
|
||||
if topic != "wesnoth.error":
|
||||
topics.append((topic, tt, doc, subtopics))
|
||||
|
||||
def output(topics, level):
|
||||
"""Output the given topics in wiki format, in a given heading level."""
|
||||
color = 0
|
||||
topics.sort()
|
||||
for topic, tt, doc, subtopics in topics:
|
||||
dot = topic.rfind(".")
|
||||
if level == 1:
|
||||
print "==", topic[dot + 1:], "module reference =="
|
||||
print "''This is an automatically generated reference, but feel " +\
|
||||
"free to edit it - changes will not be overwritten but " +\
|
||||
"instead reviewed and included in the next version.''"
|
||||
print doc or "..."
|
||||
if subtopics:
|
||||
funcs = []
|
||||
others = []
|
||||
for s in subtopics:
|
||||
if s[1] == "builtin_function_or_method":
|
||||
funcs.append(s)
|
||||
else:
|
||||
others.append(s)
|
||||
if funcs:
|
||||
print "=== Functions ==="
|
||||
print "{|"
|
||||
output(funcs, 3)
|
||||
print "|}"
|
||||
output(others, 2)
|
||||
elif level == 2:
|
||||
print "===", topic[dot + 1:], "==="
|
||||
print doc or "..."
|
||||
if subtopics:
|
||||
print "{|"
|
||||
output(subtopics, 3)
|
||||
print "|}"
|
||||
elif level == 3:
|
||||
options = " valign='top'"
|
||||
if color: options += " bgcolor='#FBF5EA'"
|
||||
print "|-" + options
|
||||
color = not color
|
||||
if tt in ["method_descriptor", "builtin_function_or_method"]:
|
||||
suffix = ""
|
||||
prefix = ""
|
||||
|
||||
if doc and doc.startswith("Parameters:"):
|
||||
l = doc.find("\n")
|
||||
if l == -1: l = len(doc) - 1
|
||||
suffix = "(" + doc[11:l].strip() + ")"
|
||||
doc = doc[l + 1:]
|
||||
else:
|
||||
suffix = "()"
|
||||
|
||||
if doc and doc.startswith("Returns:"):
|
||||
l = doc.find("\n")
|
||||
if l == -1: l = len(doc) - 1
|
||||
prefix = doc[8:l].strip() + " = "
|
||||
doc = doc[l + 1:]
|
||||
|
||||
print "|'''%s()'''" % topic[dot + 1:]
|
||||
print "|<code>%s%s%s</code>\n\n" % (prefix, topic[dot + 1:], suffix) +\
|
||||
(doc and doc.replace("\n", "\n\n") or "...")
|
||||
else:
|
||||
print "|'''%s'''\n|%s" % (topic[dot + 1:],
|
||||
(doc and doc.replace("\n", "\n\n") or "..."))
|
||||
|
||||
if __name__ == "__main__":
|
||||
import os
|
||||
# If we are run as script, run wesnoth with the --python-api switch.
|
||||
os.system("src/wesnoth --python-api")
|
||||
else:
|
||||
# If we are run as a python script, output the documentation to stdout.
|
||||
import wesnoth
|
||||
topics = []
|
||||
myhelp("wesnoth", topics)
|
||||
output(topics, 1)
|
|
@ -1,50 +0,0 @@
|
|||
import re, os, safe
|
||||
|
||||
whitelisted = ["wesnoth", "heapq", "random", "math", "string", "re"]
|
||||
rex = re.compile(r"^import\s+(.*)", re.M)
|
||||
modules = {}
|
||||
|
||||
def include(matchob):
|
||||
"""
|
||||
Regular expression callback. Handles a single import statement, returning
|
||||
the included code.
|
||||
"""
|
||||
names = [x.strip() for x in matchob.group(1).split(",")]
|
||||
r = ""
|
||||
for name in names:
|
||||
if name in whitelisted:
|
||||
modules[name] = __import__(name)
|
||||
continue
|
||||
for path in pathes:
|
||||
includefile = os.path.join(path, name)
|
||||
try:
|
||||
code = parse_file(includefile + ".py")
|
||||
break
|
||||
except IOError:
|
||||
pass
|
||||
else:
|
||||
raise safe.SafeException("Could not include %s." % name)
|
||||
return None
|
||||
r += code
|
||||
return r
|
||||
|
||||
def parse_file(name):
|
||||
"""
|
||||
Simple pre-parsing of scripts, all it does is allow importing other scripts.
|
||||
"""
|
||||
abspath = os.path.abspath(name)
|
||||
if abspath in already: return ""
|
||||
already[abspath] = 1
|
||||
code = file(abspath).read().replace(chr(13), "")
|
||||
code = rex.sub(include, code)
|
||||
return code
|
||||
|
||||
# If you want to disable safe python, use this instead:
|
||||
#
|
||||
# def parse(name): return open(name).read(), {}
|
||||
def parse(name):
|
||||
global already, modules
|
||||
already = {}
|
||||
modules = {}
|
||||
return parse_file(name), modules
|
||||
|
134
data/ais/safe.py
134
data/ais/safe.py
|
@ -1,134 +0,0 @@
|
|||
"""An attempt at creating a safe_exec for python.
|
||||
|
||||
This file is public domain and is not suited for any serious purpose.
|
||||
This code is not guaranteed to work. Use at your own risk!
|
||||
Beware! Trust no one!
|
||||
|
||||
Please e-mail philhassey@yahoo.com if you find any security holes.
|
||||
svn://www.imitationpickles.org/pysafe/trunk
|
||||
|
||||
See README.txt, NOTES.txt, CHANGES.txt for more details.
|
||||
"""
|
||||
import compiler
|
||||
import __builtin__
|
||||
|
||||
class SafeException(Exception):
|
||||
"""Base class for Safe Exceptions"""
|
||||
def __init__(self,*value):
|
||||
self.value = str(value)
|
||||
def __str__(self):
|
||||
return self.value
|
||||
class CheckNodeException(SafeException):
|
||||
"""AST Node class is not in the whitelist."""
|
||||
pass
|
||||
class CheckStrException(SafeException):
|
||||
"""A string in the AST looks insecure."""
|
||||
pass
|
||||
class RunBuiltinException(SafeException):
|
||||
"""During the run a non-whitelisted builtin was called."""
|
||||
pass
|
||||
|
||||
_NODE_CLASS_OK = [
|
||||
'Add', 'And', 'AssAttr', 'AssList', 'AssName', 'AssTuple',
|
||||
'Assert', 'Assign','AugAssign', 'Bitand', 'Bitor', 'Bitxor', 'Break',
|
||||
'CallFunc', 'Class', 'Compare', 'Const', 'Continue',
|
||||
'Dict', 'Discard', 'Div', 'Ellipsis', 'Expression', 'FloorDiv',
|
||||
'For', 'Function', 'Getattr', 'If', 'Keyword',
|
||||
'LeftShift', 'List', 'ListComp', 'ListCompFor', 'ListCompIf', 'Mod',
|
||||
'Module', 'Mul', 'Name', 'Node', 'Not', 'Or', 'Pass', 'Power',
|
||||
'Print', 'Printnl', 'Return', 'RightShift', 'Slice', 'Sliceobj',
|
||||
'Stmt', 'Sub', 'Subscript', 'Tuple', 'UnaryAdd', 'UnarySub', 'While',
|
||||
]
|
||||
_NODE_ATTR_OK = []
|
||||
_STR_OK = ['__init__']
|
||||
_STR_NOT_CONTAIN = ['__']
|
||||
_STR_NOT_BEGIN = ['im_','func_','tb_','f_','co_',]
|
||||
|
||||
## conservative settings
|
||||
#_NODE_ATTR_OK = ['flags']
|
||||
#_STR_NOT_CONTAIN = ['_']
|
||||
#_STR_NOT_BEGIN = []
|
||||
|
||||
def _check_node(node):
|
||||
if node.__class__.__name__ not in _NODE_CLASS_OK:
|
||||
raise CheckNodeException(node.lineno,node.__class__.__name__)
|
||||
for k,v in node.__dict__.items():
|
||||
if k in _NODE_ATTR_OK: continue
|
||||
if v in _STR_OK: continue
|
||||
if type(v) not in [str,unicode]: continue
|
||||
for s in _STR_NOT_CONTAIN:
|
||||
if s in v: raise CheckStrException(node.lineno,k,v)
|
||||
for s in _STR_NOT_BEGIN:
|
||||
if v[:len(s)] == s: raise CheckStrException(node.lineno,k,v)
|
||||
for child in node.getChildNodes():
|
||||
_check_node(child)
|
||||
|
||||
def _check_ast(code):
|
||||
ast = compiler.parse(code)
|
||||
_check_node(ast)
|
||||
|
||||
_BUILTIN_OK = [
|
||||
'__debug__','quit','exit',
|
||||
'Warning',
|
||||
'None','True','False',
|
||||
'abs', 'bool', 'callable', 'cmp', 'complex', 'dict', 'divmod', 'filter',
|
||||
'float', 'frozenset', 'hex', 'int', 'isinstance', 'issubclass', 'len',
|
||||
'list', 'long', 'map', 'max', 'min', 'object', 'oct', 'pow', 'range',
|
||||
'repr', 'round', 'set', 'slice', 'str', 'sum', 'tuple', 'xrange', 'zip',
|
||||
]
|
||||
|
||||
_BUILTIN_STR = [
|
||||
'copyright','credits','license','__name__','__doc__',
|
||||
]
|
||||
|
||||
def _builtin_fnc(k):
|
||||
def fnc(*vargs,**kargs):
|
||||
raise RunBuiltinException(k)
|
||||
return fnc
|
||||
_builtin_globals = None
|
||||
_builtin_globals_r = None
|
||||
def _builtin_init():
|
||||
global _builtin_globals, _builtin_globals_r
|
||||
if _builtin_globals != None: return
|
||||
_builtin_globals_r = __builtin__.__dict__.copy()
|
||||
r = _builtin_globals = {}
|
||||
for k in __builtin__.__dict__.keys():
|
||||
v = None
|
||||
if k in _BUILTIN_OK: v = __builtin__.__dict__[k]
|
||||
elif k in _BUILTIN_STR: v = ''
|
||||
else: v = _builtin_fnc(k)
|
||||
r[k] = v
|
||||
def _builtin_destroy():
|
||||
_builtin_init()
|
||||
for k,v in _builtin_globals.items():
|
||||
__builtin__.__dict__[k] = v
|
||||
def _builtin_restore():
|
||||
for k,v in _builtin_globals_r.items():
|
||||
__builtin__.__dict__[k] = v
|
||||
|
||||
def safe_check(code):
|
||||
"""Check the code to be safe."""
|
||||
return _check_ast(code)
|
||||
|
||||
def safe_run(code,context=None):
|
||||
"""Exec code with only safe builtins on."""
|
||||
if context == None: context = {}
|
||||
|
||||
_builtin_destroy()
|
||||
try:
|
||||
#exec code in _builtin_globals,context
|
||||
context['__builtins__'] = _builtin_globals
|
||||
exec code in context
|
||||
_builtin_restore()
|
||||
except:
|
||||
_builtin_restore()
|
||||
raise
|
||||
|
||||
# If you want to disable safe python, use this instead:
|
||||
#
|
||||
# def safe_exec(code, context = None): exec code in context
|
||||
def safe_exec(code, context = None):
|
||||
"""Check the code to be safe, then run it with only safe builtins on."""
|
||||
safe_check(code)
|
||||
safe_run(code,context)
|
||||
|
|
@ -1,396 +0,0 @@
|
|||
#!WPY
|
||||
|
||||
"""This is a rather simple minded example of a python AI."""
|
||||
|
||||
import wesnoth, heapq, random
|
||||
|
||||
def pos(location):
|
||||
"""Just a helper function for printing positions in debug messages."""
|
||||
return "(%d, %d)" % (1 + location.x, 1 + location.y)
|
||||
|
||||
def debug(string):
|
||||
pass
|
||||
|
||||
class AI:
|
||||
"""A class representing our AI."""
|
||||
|
||||
def __init__(self):
|
||||
"""This class is constructed once for each turn of the AI. To get
|
||||
persistent variables across terms, which also are saved when the game is
|
||||
saved, use set_variable and get_variable."""
|
||||
|
||||
self.team = wesnoth.get_current_team()
|
||||
self.village_radius = 25
|
||||
self.scout_villages = 3
|
||||
|
||||
self.recruit()
|
||||
|
||||
self.fight()
|
||||
|
||||
self.conquer()
|
||||
|
||||
def conquer(self):
|
||||
"""Try to capture villages."""
|
||||
villages = self.find_villages()
|
||||
units = wesnoth.get_destinations_by_unit().keys()
|
||||
|
||||
# Construct a sorted list of (distance, unit, village) triples.
|
||||
queue = []
|
||||
for village in villages:
|
||||
for unit in units:
|
||||
d = self.get_distance(unit, village)
|
||||
if d != None: heapq.heappush(queue, (d, unit, village))
|
||||
|
||||
# Now assign units to villages, and move them.
|
||||
while queue:
|
||||
d, unit, village = heapq.heappop(queue)
|
||||
if unit in units and village in villages:
|
||||
units.remove(unit)
|
||||
villages.remove(village)
|
||||
self.go_to(unit, village)
|
||||
|
||||
if not units: break
|
||||
if not villages: break
|
||||
|
||||
def cumulate_damage(self, cumulated, hitpoints, new_damage):
|
||||
cumulated2 = {}
|
||||
for already, ap in cumulated.iteritems():
|
||||
for hp, probability in new_damage.iteritems():
|
||||
damage = int(already + hitpoints - hp)
|
||||
cumulated2[damage] = cumulated2.get(damage, 0) + ap * probability
|
||||
return cumulated2
|
||||
|
||||
def danger_estimate(self, unit, where, enemy):
|
||||
"""Get some crude indication about how unsafe it is for unit to get
|
||||
attacked by enemy at where."""
|
||||
|
||||
scores = []
|
||||
u = wesnoth.get_units()[unit]
|
||||
e = wesnoth.get_units()[enemy]
|
||||
u_defense = u.defense_modifier(wesnoth.get_map(), where)
|
||||
e_defense = e.defense_modifier(wesnoth.get_map(), enemy)
|
||||
|
||||
u_bonus = 100 - (u.type().alignment - 1) * wesnoth.get_gamestatus().lawful_bonus
|
||||
e_bonus = 100 - (e.type().alignment - 1) * wesnoth.get_gamestatus().lawful_bonus
|
||||
|
||||
for attack in e.attacks():
|
||||
score = attack.damage * attack.num_attacks * e_bonus / 100
|
||||
score *= u_defense
|
||||
score *= u.damage_against(attack) / 100
|
||||
|
||||
back = []
|
||||
for retaliation in u.attacks():
|
||||
if attack.range == retaliation.range:
|
||||
x = retaliation.damage * retaliation.num_attacks * u_bonus / 100
|
||||
x *= e_defense
|
||||
x *= e.damage_against(retaliation) / 100
|
||||
back.append(x)
|
||||
|
||||
if back:
|
||||
r = max(back)
|
||||
score -= r
|
||||
heapq.heappush(scores, score)
|
||||
|
||||
return scores[0]
|
||||
|
||||
def danger(self, unit, location):
|
||||
"""Try to estimate danger of moving unit to location."""
|
||||
attackers = []
|
||||
for enemy, destinations in wesnoth.get_enemy_destinations_by_unit():
|
||||
for tile in wesnoth.get_adjacent_tiles(unit):
|
||||
if tile in destinations:
|
||||
heuristic = danger_estimate(unitm, location, enemy)
|
||||
if heuristic > 0:
|
||||
heapq.heappush(attackers, (-heuristic, enemy, tile))
|
||||
result = 0
|
||||
already = {}
|
||||
while attackers:
|
||||
danger, enemy, tile = heapq.heappop(attackers)
|
||||
if not already[enemy] and not already[tile]:
|
||||
danger = -danger
|
||||
result += danger
|
||||
already[enemy] = 1
|
||||
already[tile] = 1
|
||||
return result
|
||||
|
||||
def fight(self):
|
||||
"""Attack enemies."""
|
||||
enemies = wesnoth.get_enemy_destinations_by_unit().keys()
|
||||
units = wesnoth.get_destinations_by_unit().keys()
|
||||
|
||||
# Get a list of all units we can possibly kill and their chance to kill.
|
||||
# This is just a heuristic, ignoring ZoC and unit placement.
|
||||
kills = []
|
||||
for enemy in enemies:
|
||||
e = wesnoth.get_units()[enemy]
|
||||
k = {0: 1.0}
|
||||
for unit, destinations in wesnoth.get_destinations_by_unit().iteritems():
|
||||
u = wesnoth.get_units()[unit]
|
||||
for tile in wesnoth.get_adjacent_tiles(enemy):
|
||||
if tile in destinations:
|
||||
own_hp, enemy_hp = u.attack_statistics(tile, enemy)
|
||||
k = self.cumulate_damage(k, e.hitpoints, enemy_hp)
|
||||
ctk = 0
|
||||
for damage, p in k.iteritems():
|
||||
if damage >= e.hitpoints:
|
||||
ctk += p
|
||||
if ctk:
|
||||
heapq.heappush(kills, (-ctk, enemy))
|
||||
|
||||
# Now find positions from where own units can attack the to be killed
|
||||
# enemies.
|
||||
attacks = []
|
||||
while kills:
|
||||
ctk, enemy = heapq.heappop(kills)
|
||||
e = wesnoth.get_units()[enemy]
|
||||
ctk = -ctk
|
||||
for tile in wesnoth.get_adjacent_tiles(enemy):
|
||||
for unit in wesnoth.get_units_by_destination().get(tile, []):
|
||||
u = wesnoth.get_units()[unit]
|
||||
own_hp, enemy_hp = u.attack_statistics(tile, enemy)
|
||||
score = e.hitpoints - sum([x[0] * x[1] for x in enemy_hp.iteritems()])
|
||||
score -= u.hitpoints - sum([x[0] * x[1] for x in own_hp.iteritems()])
|
||||
|
||||
# This is so if there are two equally good attack
|
||||
# possibilities, we chose the one on better terrain.
|
||||
score *= 50 / u.defense_modifier(tile)
|
||||
|
||||
heapq.heappush(attacks, (-score, unit, tile, enemy))
|
||||
#print own_hp, enemy_hp
|
||||
debug("Score for %s at %s: %s<->%s: %f [%s]" % (u.name,
|
||||
pos(unit), pos(tile), pos(enemy), score, e.name))
|
||||
|
||||
# Now assign units to enemies, and move and attack.
|
||||
while attacks:
|
||||
score, unit, tile, enemy = heapq.heappop(attacks)
|
||||
score = -score
|
||||
|
||||
if unit in units and enemy in enemies:
|
||||
#try:
|
||||
loc = wesnoth.move_unit(unit, tile)
|
||||
#except ValueError:
|
||||
# loc = None
|
||||
if loc == tile:
|
||||
e = wesnoth.get_units()[enemy]
|
||||
wesnoth.attack_unit(tile, enemy)
|
||||
if not e.is_valid:
|
||||
enemies.remove(enemy)
|
||||
units.remove(unit)
|
||||
if not units: break
|
||||
|
||||
def recruit(self):
|
||||
"""Recruit units."""
|
||||
|
||||
# Check if there is any gold left first.
|
||||
cheapest = min([x.cost for x in self.team.recruits()])
|
||||
if self.team.gold < cheapest: return
|
||||
|
||||
# Find all keeps in the map.
|
||||
keeps = self.find_keeps()
|
||||
|
||||
# Find our leader.
|
||||
leader = None
|
||||
for location, unit in wesnoth.get_units().iteritems():
|
||||
if unit.side == self.team.side and unit.can_recruit:
|
||||
leader = location
|
||||
break
|
||||
|
||||
# Get number of villages to capture near to the leader.
|
||||
villages = len([x for x in self.find_villages()
|
||||
if leader.distance_to(x) < self.village_radius])
|
||||
|
||||
units_recruited = int(wesnoth.get_variable("units_recruited") or 0)
|
||||
|
||||
def attack_score(u1, u2):
|
||||
"""Some crude score of u1 attacking u2."""
|
||||
maxdeal = 0
|
||||
for attack in u1.attacks():
|
||||
deal = attack.damage * attack.num_attacks
|
||||
deal *= u2.damage_from(attack) / 100.0
|
||||
for defense in u2.attacks():
|
||||
if attack.range == defense.range:
|
||||
receive = defense.damage * defense.num_attacks
|
||||
receive *= u1.damage_from(defense) / 100.0
|
||||
deal -= receive
|
||||
if deal > maxdeal: maxdeal = deal
|
||||
return maxdeal
|
||||
|
||||
def recruit_score(recruit, speed, defense, aggression, resistance):
|
||||
"""Score for recruiting the given unit type."""
|
||||
need_for_speed = 3 * (villages / self.scout_villages -
|
||||
units_recruited)
|
||||
if need_for_speed < 0: need_for_speed = 0
|
||||
v = speed * need_for_speed + defense * 0.1 + aggression + resistance
|
||||
v += 1
|
||||
if v < 1: v = 1
|
||||
return v
|
||||
|
||||
# Try to figure out which units are good in this map.
|
||||
map = wesnoth.get_map()
|
||||
recruits = self.team.recruits()
|
||||
recruits_list = []
|
||||
for recruit in recruits:
|
||||
speed = 0.0
|
||||
defense = 0.0
|
||||
n = map.x * map.y
|
||||
for y in range(map.y):
|
||||
for x in range(map.x):
|
||||
location = wesnoth.get_location(x, y)
|
||||
speed += recruit.movement_cost(location)
|
||||
defense += 100 - recruit.defense_modifier(location)
|
||||
speed = recruit.movement * n / speed
|
||||
defense /= n
|
||||
|
||||
aggression = 0.0
|
||||
resistance = 0.0
|
||||
enemies = wesnoth.get_enemy_destinations_by_unit().keys()
|
||||
n = len(enemies)
|
||||
for location in enemies:
|
||||
enemy = wesnoth.get_units()[location]
|
||||
aggression += attack_score(recruit, enemy)
|
||||
resistance -= attack_score(enemy, recruit)
|
||||
aggression /= n
|
||||
resistance /= n
|
||||
|
||||
debug("%s: speed: %f, defense: %f, aggression: %f, resistance: %f" %
|
||||
(recruit.name, speed, defense, aggression, resistance))
|
||||
|
||||
recruits_list.append((recruit, speed, defense, aggression, resistance))
|
||||
|
||||
# Now recruit.
|
||||
for location, unit in wesnoth.get_units().iteritems():
|
||||
if unit.side == self.team.side and unit.can_recruit:
|
||||
|
||||
keepsort = []
|
||||
for keep in keeps:
|
||||
heapq.heappush(keepsort, (location.distance_to(keep), keep))
|
||||
|
||||
keep = keepsort[0][1]
|
||||
|
||||
self.go_to(location, keep)
|
||||
for i in range(6): # up to 6 units (TODO: can be more)
|
||||
# Get a random, weighted unit type from the available.
|
||||
heap = []
|
||||
total_v = 0
|
||||
for r in recruits_list:
|
||||
v = recruit_score(*r)
|
||||
v *= v * v
|
||||
total_v += v
|
||||
heapq.heappush(heap, (-v, r[0]))
|
||||
r = random.uniform(0, total_v)
|
||||
while 1:
|
||||
v, recruit = heapq.heappop(heap)
|
||||
debug("%d %d" % (r, v))
|
||||
r += v
|
||||
if r <= 0: break
|
||||
|
||||
# Try to recruit it on the adjacent tiles
|
||||
# TODO: actually, it should just use the nearest possible
|
||||
# location
|
||||
for position in wesnoth.get_adjacent_tiles(location):
|
||||
if wesnoth.recruit_unit(recruit.name, position):
|
||||
break
|
||||
else:
|
||||
# was not possible -> we're done
|
||||
break
|
||||
units_recruited += 1
|
||||
wesnoth.set_variable("units_recruited", str(units_recruited))
|
||||
|
||||
def find_villages(self):
|
||||
"""Find all villages which are unowned or owned by enemies."""
|
||||
villages = []
|
||||
m = wesnoth.get_map()
|
||||
for x in range(m.x):
|
||||
for y in range(m.y):
|
||||
location = wesnoth.get_location(x, y)
|
||||
if wesnoth.get_map().is_village(location):
|
||||
for team in wesnoth.get_teams():
|
||||
# does it alreadey belong to use or an ally?
|
||||
if team.owns_village(location) and not team.is_enemy:
|
||||
break
|
||||
else:
|
||||
# no, either it belongs to an enemy or to nobody
|
||||
villages.append(location)
|
||||
|
||||
return villages
|
||||
|
||||
def find_keeps(self):
|
||||
"""Find keep locations."""
|
||||
keeps = []
|
||||
m = wesnoth.get_map()
|
||||
for x in range(m.x):
|
||||
for y in range(m.y):
|
||||
location = wesnoth.get_location(x, y)
|
||||
if wesnoth.get_map().is_keep(location):
|
||||
keeps.append(location)
|
||||
return keeps
|
||||
|
||||
def get_distance(self, location, target, must_reach = False):
|
||||
"""Find out how many turns it takes the unit at location to reach target."""
|
||||
if location == target: return 0
|
||||
unit = wesnoth.get_units()[location]
|
||||
path = unit.find_path(location, target, 100)
|
||||
extra = 0
|
||||
if not path:
|
||||
extra = 1
|
||||
if must_reach: return None
|
||||
for adjacent in wesnoth.get_adjacent_tiles(target):
|
||||
# Consider 5 turns worth of movement of this unit.
|
||||
path = unit.find_path(location, adjacent,
|
||||
unit.type().movement * 5)
|
||||
if path: break
|
||||
else:
|
||||
return None
|
||||
l = 0
|
||||
for location in path:
|
||||
l += unit.movement_cost(location)
|
||||
l -= unit.movement_left
|
||||
l /= unit.type().movement
|
||||
l += 1 + extra
|
||||
return l
|
||||
|
||||
def attack(self, location, enemy):
|
||||
"""Attack an enemy unit."""
|
||||
wesnoth.attack_unit(location, enemy)
|
||||
|
||||
def go_to(self, location, target, must_reach = False):
|
||||
"""Make a unit at the given location go to the given target.
|
||||
Returns the reached position.
|
||||
"""
|
||||
if location == target: return location
|
||||
|
||||
# If target is occupied, try to go near it
|
||||
unit_locations = wesnoth.get_units().keys()
|
||||
if target in unit_locations:
|
||||
if must_reach: return location
|
||||
adjacent = wesnoth.get_adjacent_tiles(target)
|
||||
targets = [x for x in adjacent if not x in unit_locations]
|
||||
if targets:
|
||||
target = targets[0]
|
||||
else:
|
||||
return location
|
||||
|
||||
# find a path
|
||||
for l, unit in wesnoth.get_units().iteritems():
|
||||
if location == l:
|
||||
path = unit.find_path(location, target, unit.type().movement * 5)
|
||||
break
|
||||
else:
|
||||
return location
|
||||
|
||||
if path:
|
||||
possible_destinations = wesnoth.get_destinations_by_unit().get(location, [])
|
||||
if must_reach:
|
||||
if not target in path: return location
|
||||
if not target in possible_destinations: return location
|
||||
|
||||
# find first reachable position in reversed path
|
||||
path.reverse()
|
||||
|
||||
for p in path:
|
||||
if p in possible_destinations and not p in unit_locations:
|
||||
location = wesnoth.move_unit(location, p)
|
||||
return location
|
||||
return location
|
||||
|
||||
AI()
|
|
@ -1,68 +0,0 @@
|
|||
#!WPY
|
||||
import wesnoth
|
||||
|
||||
class AI:
|
||||
def __init__(self):
|
||||
self.do()
|
||||
|
||||
def do(self):
|
||||
# loop over all enemy units
|
||||
for enemy_loc, ed in wesnoth.get_enemy_destinations_by_unit().iteritems():
|
||||
target_unit = wesnoth.get_units()[enemy_loc]
|
||||
# see if unit is the leader of player's side
|
||||
if target_unit.side == 1 and target_unit.can_recruit == 1:
|
||||
# if so, get adjacent locations
|
||||
for unit_loc, destinations in wesnoth.get_destinations_by_unit().iteritems():
|
||||
attacked_flag = False
|
||||
for destination in destinations:
|
||||
if destination.adjacent_to(enemy_loc):
|
||||
wesnoth.move_unit(unit_loc, destination)
|
||||
wesnoth.attack_unit(destination, enemy_loc)
|
||||
attacked_flag = True
|
||||
break
|
||||
if (not attacked_flag):
|
||||
new_loc = self.go_to(unit_loc, enemy_loc, False)
|
||||
if new_loc.adjacent_to(enemy_loc):
|
||||
wesnoth.attack_unit(new_loc, enemy_loc)
|
||||
|
||||
def go_to(self, location, target, must_reach = False):
|
||||
"""Make a unit at the given location go to the given target.
|
||||
Returns the reached position.
|
||||
"""
|
||||
if location == target: return location
|
||||
|
||||
# If target is occupied, try to go near it
|
||||
unit_locations = wesnoth.get_units().keys()
|
||||
if target in unit_locations:
|
||||
if must_reach: return location
|
||||
adjacent = wesnoth.get_adjacent_tiles(target)
|
||||
targets = [x for x in adjacent if not x in unit_locations]
|
||||
if targets:
|
||||
target = targets[0]
|
||||
else:
|
||||
return location
|
||||
|
||||
# find a path
|
||||
for l, unit in wesnoth.get_units().iteritems():
|
||||
if location == l:
|
||||
path = unit.find_path(location, target, 1000)
|
||||
break
|
||||
else:
|
||||
return location
|
||||
|
||||
if path:
|
||||
possible_destinations = wesnoth.get_destinations_by_unit().get(location, [])
|
||||
if must_reach:
|
||||
if not target in path: return location
|
||||
if not target in possible_destinations: return location
|
||||
|
||||
# find first reachable position in reversed path
|
||||
path.reverse()
|
||||
|
||||
for p in path:
|
||||
if p in possible_destinations and not p in unit_locations:
|
||||
location = wesnoth.move_unit(location, p)
|
||||
return location
|
||||
return location
|
||||
|
||||
AI()
|
|
@ -77,11 +77,6 @@
|
|||
no_leader=yes
|
||||
fog=no
|
||||
shroud=no
|
||||
# kamikaze.py heads straight for the leader of side 1 and attacks
|
||||
[ai]
|
||||
ai_algorithm=python_ai
|
||||
python_script="../campaigns/Descent_Into_Darkness/ais/kamikaze.py"
|
||||
[/ai]
|
||||
[/side]
|
||||
|
||||
[event]
|
||||
|
|
|
@ -54,7 +54,6 @@ wesnoth_source = \
|
|||
ai_dfool.cpp \
|
||||
ai_attack.cpp \
|
||||
ai_move.cpp \
|
||||
ai_python.cpp \
|
||||
ai_village.cpp \
|
||||
animated_game.cpp \
|
||||
attack_prediction.cpp \
|
||||
|
@ -325,11 +324,6 @@ AM_CXXFLAGS = -I $(srcdir)/sdl_ttf -I../intl -I$(top_srcdir)/intl @SDL_CFLAGS@
|
|||
AM_CFLAGS = -I $(srcdir)/sdl_ttf -I../intl -I$(top_srcdir)/intl @SDL_CFLAGS@ -DWESNOTH_PATH=\"$(pkgdatadir)\" \
|
||||
-DLOCALEDIR=\"$(LOCALEDIR)\" -DHAS_RELATIVE_LOCALEDIR=$(HAS_RELATIVE_LOCALEDIR)
|
||||
|
||||
if PYTHON
|
||||
AM_CXXFLAGS += @PYTHON_CFLAGS@
|
||||
AM_CFLAGS += @PYTHON_CFLAGS@
|
||||
endif
|
||||
|
||||
if FRIBIDI
|
||||
AM_CXXFLAGS += -DHAVE_FRIBIDI @FRIBIDI_CFLAGS@
|
||||
AM_CFLAGS += -DHAVE_FRIBIDI @FRIBIDI_CFLAGS@
|
||||
|
|
17
src/ai.cpp
17
src/ai.cpp
|
@ -20,9 +20,6 @@
|
|||
#include "ai.hpp"
|
||||
#include "ai2.hpp"
|
||||
#include "ai_dfool.hpp"
|
||||
#ifdef HAVE_PYTHON
|
||||
#include "ai_python.hpp"
|
||||
#endif
|
||||
#include "actions.hpp"
|
||||
#include "dialogs.hpp"
|
||||
#include "game_config.hpp"
|
||||
|
@ -184,10 +181,6 @@ std::vector<std::string> get_available_ais()
|
|||
ais.push_back("sample_ai");
|
||||
//ais.push_back("idle_ai");
|
||||
ais.push_back("dfool_ai");
|
||||
#ifdef HAVE_PYTHON
|
||||
std::vector<std::string> scripts = python_ai::get_available_scripts();
|
||||
ais.insert(ais.end(), scripts.begin(), scripts.end());
|
||||
#endif
|
||||
return ais;
|
||||
}
|
||||
|
||||
|
@ -208,18 +201,8 @@ ai_interface* create_ai(const std::string& name, ai_interface::info& info)
|
|||
// return new advanced_ai(info);
|
||||
else if(name == "ai2")
|
||||
return new ai2(info);
|
||||
else if(name == "python_ai")
|
||||
#ifdef HAVE_PYTHON
|
||||
return new python_ai(info);
|
||||
#else
|
||||
{
|
||||
LOG_STREAM(err, ai) << "No Python AI support available in this Wesnoth build!\n";
|
||||
return new ai2(info);
|
||||
}
|
||||
#endif
|
||||
else if(name != "")
|
||||
LOG_STREAM(err, ai) << "AI not found: '" << name << "'\n";
|
||||
|
||||
return new ai(info);
|
||||
}
|
||||
|
||||
|
|
2023
src/ai_python.cpp
2023
src/ai_python.cpp
File diff suppressed because it is too large
Load diff
|
@ -1,101 +0,0 @@
|
|||
/* $Id$ */
|
||||
/*
|
||||
Copyright (C) 2007 - 2008
|
||||
Part of the Battle for Wesnoth Project http://www.wesnoth.org/
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License version 2
|
||||
or at your option any later version.
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY.
|
||||
|
||||
See the COPYING file for more details.
|
||||
*/
|
||||
|
||||
//! @file ai_python.hpp
|
||||
//!
|
||||
|
||||
#ifndef AI_PYTHON_HPP_INCLUDED
|
||||
#define AI_PYTHON_HPP_INCLUDED
|
||||
|
||||
#include "ai_interface.hpp"
|
||||
#include "menu_events.hpp"
|
||||
#undef _POSIX_C_SOURCE // avoids a spurious compiler warning
|
||||
#include <Python.h>
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
const unit_type* unit_type_;
|
||||
} wesnoth_unittype;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
const team* team_;
|
||||
} wesnoth_team;
|
||||
|
||||
typedef struct {
|
||||
PyObject_HEAD
|
||||
const unit* unit_;
|
||||
} wesnoth_unit;
|
||||
|
||||
#define W(name) static PyObject *wrapper_##name(PyObject* self, PyObject* args)
|
||||
class python_ai : public ai_interface
|
||||
{
|
||||
public:
|
||||
python_ai(ai_interface::info& info);
|
||||
virtual ~python_ai();
|
||||
virtual void play_turn();
|
||||
|
||||
static PyObject* wrapper_unit_movement_cost(wesnoth_unit*, PyObject* args);
|
||||
static PyObject* wrapper_unit_defense_modifier(wesnoth_unit*, PyObject* args);
|
||||
static PyObject* wrapper_unittype_movement_cost(wesnoth_unittype*, PyObject* args);
|
||||
static PyObject* wrapper_unittype_defense_modifier(wesnoth_unittype*, PyObject* args);
|
||||
|
||||
W(team_targets);
|
||||
W(get_units);
|
||||
W(log_message);
|
||||
W(get_location);
|
||||
W(get_map);
|
||||
W(get_teams);
|
||||
W(get_current_team);
|
||||
W(get_src_dst);
|
||||
W(get_dst_src);
|
||||
W(get_enemy_src_dst);
|
||||
W(get_enemy_dst_src);
|
||||
W(move_unit);
|
||||
W(attack_unit);
|
||||
W(get_adjacent_tiles);
|
||||
W(recruit_unit);
|
||||
W(get_gamestatus);
|
||||
W(set_variable);
|
||||
W(get_variable);
|
||||
W(get_version);
|
||||
W(raise_user_interact);
|
||||
W(test_move);
|
||||
|
||||
static PyObject* unittype_advances_to( wesnoth_unittype* type, PyObject* args );
|
||||
static PyObject* wrapper_team_recruits( wesnoth_team* team, PyObject* args );
|
||||
static PyObject* wrapper_unit_find_path( wesnoth_unit* unit, PyObject* args );
|
||||
static PyObject* wrapper_unit_attack_statistics(wesnoth_unit* unit, PyObject* args);
|
||||
|
||||
static bool is_unit_valid(const unit* unit);
|
||||
std::vector<team>& get_teams() { return get_info().teams; }
|
||||
static std::vector<std::string> get_available_scripts();
|
||||
static void initialize_python();
|
||||
static void invoke(std::string name);
|
||||
|
||||
friend void recalculate_movemaps();
|
||||
private:
|
||||
static bool init_;
|
||||
|
||||
end_level_exception exception;
|
||||
ai_interface::move_map src_dst_;
|
||||
ai_interface::move_map dst_src_;
|
||||
std::map<location,paths> possible_moves_;
|
||||
ai_interface::move_map enemy_src_dst_;
|
||||
ai_interface::move_map enemy_dst_src_;
|
||||
std::map<location,paths> enemy_possible_moves_;
|
||||
};
|
||||
#undef W
|
||||
|
||||
#endif
|
15
src/game.cpp
15
src/game.cpp
|
@ -58,10 +58,6 @@
|
|||
#include "serialization/string_utils.hpp"
|
||||
#include "sha1.hpp"
|
||||
|
||||
#ifdef HAVE_PYTHON
|
||||
#include "ai_python.hpp"
|
||||
#endif
|
||||
|
||||
#include "wesconfig.h"
|
||||
|
||||
#include <clocale>
|
||||
|
@ -1876,9 +1872,6 @@ void game_controller::reset_defines_map()
|
|||
defines_map_["SMALL_GUI"] = preproc_define();
|
||||
#endif
|
||||
|
||||
#ifdef HAVE_PYTHON
|
||||
defines_map_["PYTHON"] = preproc_define();
|
||||
#endif
|
||||
}
|
||||
|
||||
void game_controller::play_game(RELOAD_GAME_DATA reload)
|
||||
|
@ -2007,9 +2000,6 @@ static int play_game(int argc, char** argv)
|
|||
<< " --max-fps the maximum fps the game tries to run at the value\n"
|
||||
<< " should be between the 1 and 1000, the default is 50.\n"
|
||||
<< " --path prints the name of the game data directory and exits.\n"
|
||||
#ifdef HAVE_PYTHON
|
||||
<< " --python-api prints the runtime documentation for the python API.\n"
|
||||
#endif
|
||||
<< " -r, --resolution XxY sets the screen resolution. Example: -r 800x600\n"
|
||||
<< " -t, --test runs the game in a small test scenario.\n"
|
||||
<< " -v, --version prints the game's version number and exits.\n"
|
||||
|
@ -2040,11 +2030,6 @@ static int play_game(int argc, char** argv)
|
|||
std::cout << game_config::path
|
||||
<< "\n";
|
||||
return 0;
|
||||
#ifdef HAVE_PYTHON
|
||||
} else if(val == "--python-api") {
|
||||
python_ai::invoke("documentation.py");
|
||||
return 0;
|
||||
#endif
|
||||
} else if (val.substr(0, 6) == "--log-") {
|
||||
size_t p = val.find('=');
|
||||
if (p == std::string::npos) {
|
||||
|
|
|
@ -579,17 +579,8 @@ config connect::side::get_config() const
|
|||
{
|
||||
config *ai = res.child("ai");
|
||||
if (!ai) ai = &res.add_child("ai");
|
||||
#ifdef HAVE_PYTHON
|
||||
if (ai_algorithm_.substr(ai_algorithm_.length() - 3) == ".py") {
|
||||
(*ai)["ai_algorithm"] = "python_ai";
|
||||
(*ai)["python_script"] = ai_algorithm_;
|
||||
}
|
||||
else
|
||||
#endif
|
||||
{
|
||||
if (ai_algorithm_ != "default")
|
||||
(*ai)["ai_algorithm"] = ai_algorithm_;
|
||||
}
|
||||
if (ai_algorithm_ != "default")
|
||||
(*ai)["ai_algorithm"] = ai_algorithm_;
|
||||
}
|
||||
description = N_("Computer player");
|
||||
break;
|
||||
|
|
Loading…
Add table
Reference in a new issue