PythonTestScript

From Wesnoth

See ReferencePythonAPI for complete Python API and how to make python scripts appear in the in-game AI selection.

This is the test script that ships with the game, the latest version can always be found here: http://svn.gna.org/viewcvs/wesnoth/trunk/data/ai/python/sample.py?view=markup

#!WPY
import wesnoth, random

"""This is a rather simple minded example of a python AI."""

def pos(location):
    """Just a helper function for printing positions in debug messages."""
    return "(%d, %d)" % (1 + location.x, 1 + location.y)

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.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: queue.append((d, unit, village))
        queue.sort()

        # Now assign units to villages, and move them.
        for d, unit, village in 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 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)
                        kn = {}
                        for already, ap in k.iteritems():
                            for hp, probability in enemy_hp.iteritems():
                                damage = int(already + e.hitpoints - hp)
                                kn[damage] = kn.get(damage, 0) + ap * probability
                        k = kn
            ctk = 0
            for damage, p in k.iteritems():
                if damage >= e.hitpoints:
                    ctk += p
            if ctk:
                kills.append((-ctk, enemy))
        kills.sort()

        # Now find positions from where own units can attack the to be killed
        # enemies.
        attacks = []
        for ctk, enemy in 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(k * v for k,v in enemy_hp.iteritems())
                    score -= u.hitpoints - sum(k * v for k,v 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)

                    attacks.append((-score, unit, tile, enemy))
                    #print own_hp, enemy_hp
                    print "Score for %s at %s: %s<->%s: %f [%s]" % (u.name, pos(unit), pos(tile),
                            pos(enemy), score, e.name)
        attacks.sort()

        # Now assign units to enemies, and move and attack.
        for score, unit, tile, enemy in 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."""
        keeps = self.find_keeps()

        def 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)
                for defense in u2.attacks():
                    if attack.range == defense.range:
                        receive = defense.damage * defense.num_attacks
                        receive *= u1.damage_from(defense)
                        deal -= receive
                if deal > maxdeal: maxdeal = deal
            return maxdeal

        # Try to figure out which units are good in this map.
        map = wesnoth.get_map()
        properties = {}
        for recruit in self.team.recruits():
            slowness = 0
            defense = 0
            for y in range(map.y):
                for x in range(map.x):
                    location = wesnoth.get_location(x, y)
                    slowness += recruit.movement_cost(location)
                    defense += recruit.defense_modifier(location)
            slowness /= recruit.movement

            aggression = 0
            resistance = 0
            for location in wesnoth.get_enemy_destinations_by_unit().keys():
                enemy = wesnoth.get_units()[location]
                aggression += score(recruit, enemy)
                resistance -= score(enemy, recruit)
            sys.stdout.write("%s: " % recruit.name)
            sys.stdout.write("slowness: %d, " % slowness)
            sys.stdout.write("defense: %d, " % defense)
            sys.stdout.write("aggression: %d, " % aggression)
            sys.stdout.write("resistance: %d" % resistance)
            sys.stdout.write("\n")

        for location, unit in wesnoth.get_units().iteritems():
            if unit.side == self.team.side and unit.can_recruit:

                keep = min(keeps, key=location.distance_to)

                self.go_to(location, keep)
                for i in range(6): # up to 6 units (TODO: can be more)
                    recruit = random.choice(self.team.recruits())

                    # 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

    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 already 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 x not 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 target not in path: return location
                if target not in possible_destinations: return location

            # find first reachable position in reversed path
            path.reverse()

            for p in path:
                if p in possible_destinations and p not in unit_locations:
                    location = wesnoth.move_unit(location, p)
                    return location
        return location

AI()

This page was last modified on 10 February 2011, at 00:14.