Source

hexbattle / hexbattle_units.py

#!/usr/bin/env python
# encoding: utf-8

"""Unit definitions for hexbattle scenes."""

# standard imports
from os.path import join
from random import choice
from time import time, sleep

# pyglet imports
from pyglet import image, resource, gl
from hexbattle_hex import Hexfield
from core import Sprite

# own stuff
from rpg_1d6.char import Char
# hex functions
from hexgrid import *
from core import Sprite

class Character(Hexfield, Char):
    """A character on the Hexfield. It knows the level as hexmap.

    TODO: multi-step actions: action = (identifier, time (from time()))

    >>> # Use the Character without scene and with fake hexmap
    >>> # → nonfunctional ⇒ only for testing. 
    >>> char = Character(None, {})
    >>> char.x
    0
    >>> char.hex_x
    0
    >>> char.move_to_hex(1, 2)
    >>> char.x
    54.0
    >>> char.y
    180.0
    >>> char.attack
    12
    >>> char.attack = 9
    >>> char.attack
    9
    >>> char.attack = 18
    >>> char.attack
    18
    """
    # make self.x and self.y properties, which update the grip positions.
    def __init__(self, scene, hexmap, x=0, y=0, image_path=join("graphics", "wesnoth", "wose.png"), template_data = None, source = "tag:1w6.org,2008:Human", *args, **kwds):
        super(Character, self).__init__(scene, image_path=image_path, x=x, y=y, *args, **kwds)
        Char.__init__(self, template_data = template_data, source = source, *args, **kwds)
        self.hexmap = hexmap
        self.actions = [{"name": "attack", "image": join("graphics", "wesnoth", "flame-sword.png")}]
        self.attack_targets = hex_vectors[1:]
        #: Sprites which belong to this unit and are blitted and updated by it. 
        self.status_sprites = []
        #: Actions the unit should still do.
        self.timed_actions = []
        #: The group you belong to.
        self.team = None

        # take the place
        self.hex_x, self.hex_y = coordinate_to_hexgrid(self.x, self.y)

        if not self.hexmap.get((self.hex_x, self.hex_y), False): 
            self.hexmap[(self.hex_x, self.hex_y)] = self
        else:
            hx = self.hex_x
            hy = self.hex_y
            # print self.hexmap[(hx, hy)]
            while self.hexmap.get((hx, hy), False):
                hx += 1
                if not self.hexmap.get((hx, hy), False):
                    self.move_to_hex(hx, hy)
                    break
                hy += 1
                if not self.hexmap.get((hx, hy), False):
                    self.move_to_hex(hx, hy)
                    break
            # print "place already taken"

        self.max_move = int(self._get_att_val("Speed") / 3)

    def show_wound(self, critical=False):
        """Add a wound status sprite."""
        if critical:
            wound = Sprite(join("graphics", "wesnoth", "ball-magenta-small.png"))
        else: 
            wound = Sprite(join("graphics", "wesnoth", "ring-red.png"))
        wound.offset_x = 20 - 12*len(self.status_sprites)
        wound.offset_y = 20
        self.status_sprites.append(wound)

    ## temporary → needs to get into rpg_1d6, once it’s clear what exactly I need.
    def _get_att_val(self, name):
        """get the value of the given Attribute (small wrapper)."""
        if name in self.attributes:
            return self.get_attribute_basevalue(self.attributes[name])
        else: 
            return 12
    def roll_initiative(self):
        return self.roll(self._get_att_val("Initiative"))
    
    def movement_targets(self):
        """Get all hexfields to which the Charakter could move.

        TODO: Move method into scene, as it deals with relationship between units.
        """
        targets = []
        # basic optimization: only check a hex-„square“ around the blob.
        for x in range(-self.max_move, self.max_move+1):
            for y in range(-self.max_move, self.max_move+1):
                z = - (x+y)
                dist = 0.5*(abs(x)+abs(y)+abs(z))
                if dist <= self.max_move:
                    pos = (self.hex_x + x, self.hex_y + y)
                    # don’t list those places which are already taken.
                    hexunit = self.hexmap.get(pos, None)
                    if hexunit is None or hexunit is self: 
                        targets.append(pos)
        return targets

    def _guess_damage_against(self, target):
        """Guess the damage we will do against a given target."""
        target_malus = 3 * len(self.scene.enemies_around(target)) + target.wounds[0]*3 + target.wounds[1]*6 - 3
        my_malus = 3 * len(self.scene.enemies_around(self)) + self.wounds[0]*3 + self.wounds[1]*6 - 3
        my_bonus = target_malus - my_malus
        return self.dam + my_bonus

    def guess_damage_at(self, hexpos):
        """Guess the damage we can do on the given hexfield."""
        enemies_around = [u for u in self.scene.units_around_hex(hexpos[0], hexpos[1]) if self.scene.are_enemies(self, u)]
        if not enemies_around:
            return 0
        my_malus = 3*len(enemies_around) - 3
        # only take the highest enemy malus ⇒ the most vulnerable enemy.
        m = []
        for e in enemies_around:
            m.append(3 * len(self.scene.enemies_around(e)) + e.wounds[0]*3 + e.wounds[1]*6)
        highest_target_malus = max(m)
        # the basic battle bonus against the weakest unit at this hex.
        my_bonus = highest_target_malus - my_malus
        return self.dam + my_bonus

    def best_target_hex(self):
        """Find the best hex to move to.
        
        TODO: Move method into scene, as it deals with relationship between units.
"""
        possible_targets = self.movement_targets()
        # if all places are taken, we stay here.
        if not possible_targets:
            print "ARGL! we should always be able to stay at our place."
            return self.hex_x, self.hex_y
        weighted_targets = [(self.guess_damage_at(t), t) for t in possible_targets]
        # if we have no fields with enemies around them, take the one which is closest to an enemy.
        if sum((abs(dam) for dam, t in weighted_targets)) == 0:
            enemies = self.scene.all_enemies_of(self)
            # if we have no enemies, just don’t move
            if not enemies:
                return self.hex_x, self.hex_y
            nearest = possible_targets[0]
            dist_nearest = self.distance(nearest[0], nearest[1], enemies[0].hex_x, enemies[0].hex_y)
            for field in possible_targets:
                for e in enemies:
                    dist = self.distance(field[0], field[1], e.hex_x, e.hex_y)
                    if dist < dist_nearest:
                        nearest = field
                        dist_nearest = dist
            return nearest
        # sort the target fields by 
        weighted_targets.sort()
        return weighted_targets[-1][1]

    def _select_attack_target(self):
        """Select a nearby target for attacking."""
        targets = self.scene.enemies_around(self)
        if not targets:
            return None
        targets = [(self._guess_damage_against(t), t) for t in targets]
        targets.sort()
        return targets[-1][1]

    def attack_best_target_if_possible(self):
        """Attack the best target in range, if there are targets in range."""
        target = self._select_attack_target()
        if target is None:
            return # no target ⇒ don’t act
        self._action_attack(target)

    def remove_from_hexmap(self):
        """Remove the character from the scenes hexmap."""
        if self.hexmap.get((self.hex_x, self.hex_y)) is self:
            del self.hexmap[(self.hex_x, self.hex_y)]

    def update_status_sprite_positions(self):
        """move the status sprites alongside the Character."""
        for sprite in self.status_sprites:
            sprite.x = self.x + sprite.offset_x
            sprite.y = self.y + sprite.offset_y
        

    def move_to_hex(self, hex_x, hex_y):
        """Move on the hexgrid."""
        self.remove_from_hexmap()
        if not self.hexmap.has_key((hex_x, hex_y)): 
            self.hex_x, self.hex_y = hex_x, hex_y
            self.x, self.y = hexgrid_to_coordinate(hex_x, hex_y)
            self.hexmap[(hex_x, hex_y)] = self
        else:
            print "place already taken"

    def _attack_mods_on(self, target):
        my_malus = 3 * len(self.scene.enemies_around(self)) - 3
        target_malus = 3 * len(self.scene.enemies_around(target)) - 3
        # wounds are being taken care of in the ruleset. Only the info specific to the tectics has to be given.        
        return my_malus, target_malus

    def _action_attack(self, target):
        """Attack the target."""
        my_malus, target_malus = self._attack_mods_on(target)
        won, damage_self, damage_other = self.fight_round(target, mods_self = [-my_malus], mods_other = [-target_malus])
        # print self.attack, "vs", target.attack, "mods:", my_malus, target_malus, "res:", won, damage_self, damage_other
        if won: 
            wound, critical, dam = damage_other
            if wound or critical:
                target.show_wound(critical)
        else:
            wound, critical, dam = damage_self
            if wound or critical:
                self.show_wound(critical)

        t = time()
        direction = (self.hex_x - target.hex_x, self.hex_y - target.hex_y)
        if won:
            target.timed_actions.append(("tumble", t, direction))
        else:
            self.timed_actions.append(("tumble", t, direction))

    def _action_tumble(self, action):
        """tumble back and forth for 1/3rd second after a hit."""
        name, starttime, direction = action
        t = time()
        if starttime > t:
            # not yet time to act.
            return
        if t - starttime >= 0.3:
            # move back to the original position.
            self.x, self.y = hexgrid_to_coordinate(self.hex_x, self.hex_y)
            self.timed_actions.remove(action)
            return

        # move back and forth into all hex directions
        #: an integer starttime, from 0 to 6.
        hex_dx, hex_dy = direction
        hex_dx += self.hex_x
        hex_dy += self.hex_y
        x, y = hexgrid_to_coordinate(hex_dx, hex_dy)
        real_x, real_y = hexgrid_to_coordinate(self.hex_x, self.hex_y)
        # move back and forth
        step = int(12*(t-starttime))%2
        if step: 
            self.x = real_x + 0.04*(x - real_x)
            self.y = real_y + 0.04*(y - real_y)
        else:
            self.x = real_x - 0.04*(x - real_x)
            self.y = real_y - 0.04*(y - real_y)
        

    def act(self, action):
        """Do the specified action. Called by the command wheel.

        @return: None to do no more actions and hide the CommandOverlay or a list of actions to show another action dialog."""
        if action is None:
            return
        if action["name"] == "attack":
            actions = []
            # check all neighboring fields for targets.
            for hex_x, hex_y in self.attack_targets:
                hex_x += self.hex_x
                hex_y += self.hex_y
                target = self.hexmap.get((hex_x, hex_y), None)
                if target is not None and self.scene.are_enemies(self, target):
                    a = {}
                    a["name"] = "attack target"
                    a["image"] = join("graphics", "wesnoth", "flame-sword.png")
                    a["value"] = (hex_x, hex_y)
                    actions.append(a)
                else: actions.append(None)
            return actions
        if action["name"] == "attack target":
            target = self.hexmap.get(action["value"], None)
            if target is not None:
                self._action_attack(target)
                
    def update(self):
        """Make sure we’re alive."""
        if not self.alive:
            self.hide()
            self.remove_from_hexmap()
        for action in self.timed_actions:
            if action[0] == "tumble":
                self._action_tumble(action)
        self.update_status_sprite_positions()
        for sprite in self.status_sprites:
            sprite.update()

    def draw(self):
        """Show the char and the subsprites belonging to it."""
        super(Character, self).draw()
        for sprite in self.status_sprites:
            sprite.draw()
                


if __name__ == "__main__":
    # there’s only one reason for calling this file: doctests
    from doctest import testmod
    testmod()