Commits

Arne Babenhauserheide  committed 4f73956

more refactoring.

  • Participants
  • Parent commits 55ef3c7
  • Branches hexbattle

Comments (0)

Files changed (5)

File fungus_hexbattle.py

 # hex functions
 from hexgrid import *
 
+# characters and overlays
+from fungus_hexbattle_units import *
+from fungus_hexbattle_overlays import *
+
 #### The Hexbattle Scene  ####
 
 ### Things needed for the scene
 IMAGE_TILE_SNOW = [join("wesnoth", i) for i in ["deciduous-fall-tile.png",
                                                 "deciduous-winter-tile.png"]] # "forested-deciduous-summer-hills-tile.png", "forested-hills-tile.png"
 
-class Hexfield(pyglet_sprite):
-    """A simple hexfield element which adheres to the grid.
-
-    TODO: add show_info() which shows an information overlay in the hexfield."""
-    def __init__(self, scene, image_path=join("graphics", "hex-overlay.png"), x=0, y=0, *args, **kwds): 
-        ## Sprite image
-	img = resource.image(image_path)
-        self.scene = scene
-	super(Hexfield, self).__init__(img, x=x, y=y, *args, **kwds)
-
-    def update(self):
-        pass
-
-    def to_grid_position(self, grid_x, grid_y):
-        """Move to a certain place in the grid."""
-        self.x, self.y = hexgrid_to_coordinate(grid_x, grid_y)
-
-    def move(self, x, y):
-        """Move to the place in the grid which contains the coordinate."""
-        grid_x, grid_y = coordinate_to_hexgrid(x, y)
-        self.to_grid_position(grid_x, grid_y)
-
-    def move_to_hex(self, x, y):
-        self.x, self.y = hexgrid_to_coordinate(x, y)
-
-    def show(self):
-        """Become visible."""
-        if not self in self.scene.visible:
-            self.scene.visible.append(self)
-
-    def hide(self):
-        """Become invisible."""
-        try: self.scene.visible.remove(self)
-        except ValueError: pass # not in the visible list.
-
-    def distance(self, sx, sy, tx, ty):
-        """Get the distance between this hex and the target."""
-        return hexgrid_distance(sx, sy, tx, ty)
-       
-    def distance_to(self, target):
-        """Get the distance between this hex and the target."""
-        return self.distance(self.hex_x, self.hex_y, target.hex_x, target_hex_y)
-
-
-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("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 = self.scene.core.sprite(join("wesnoth", "ball-magenta-small.png"))
-        else: 
-            wound = self.scene.core.sprite(join("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("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()
-                
             
-class Action(Hexfield):
-    """A simple action for the command overlay"""
-    def __init__(self, scene, action=None, *args, **kwds):
-        if action is None:
-            image_path=join("graphics", "hex-overlay.png")
-        else:
-            image_path=join("graphics", action["image"])
-        super(Action, self).__init__(scene, image_path=image_path,
-                                     *args, **kwds)
-        self.action = action
-        
-    def clicked(self):
-        """The action was clicked: get active and return the next step.
-
-        If we return None, the command wheel is finished."""
-        return self.action
-
-class CommandOverlay(object):
-    """An overlay which prompts the user to click actions.
-
-    >>> # Use the overlay without scene — nonfunctional ⇒ only for testing. 
-    >>> c = CommandOverlay(None)
-
-    TODO: Maybe change the overlay to allow for a hidden, yet already created overlays. Currently self.show creates a new overlay at the given position."""
-    def __init__(self, scene):
-        self.scene = scene
-        self.hex_x = 0
-        self.hex_y = 0
-        #: The fields on which we want to be.
-        self.fields = {}
-        #: The hexes we want to place stuff on, relative to our position.
-        self.hex_vectors = hex_vectors
-
-    @property
-    def is_visible(self):
-        return self.fields
-
-    def _prepare_actions(self, actions):
-        """Setup the list of actions"""
-        # hide and clear the current actions.
-        self.hide()
-        # and get new ones.
-        if actions is None:
-            # TODO: Get the default list of actions.
-            actions = [None for i in range(7)]
-        else:
-            actions = actions[:]
-            # first object is cancel.
-            actions.insert(0, None)
-            # undefined actions are None
-            actions = actions + [None for i in range(7-len(actions))]
-        for i in range(len(actions)):
-            hex_x, hex_y = self.hex_vectors[i]
-            hex_x += self.hex_x
-            hex_y += self.hex_y
-            x, y = hexgrid_to_coordinate(hex_x, hex_y)
-            action = Action(self.scene, action=actions[i], x = x, y = y)
-            self.scene.hexmap_overlay[(hex_x, hex_y)] = action
-            self.fields[(hex_x, hex_y)] = action
-
-    def draw(self):
-        for action in self.fields.values():
-            action.draw()
-    
-    def show(self, hex_x, hex_y, actions=None):
-        """Create and show the command overlay at the given position. If it gets actions, use these."""
-        self.hex_x = hex_x
-        self.hex_y = hex_y
-        cmds = self._prepare_actions(actions)
-
-    def hide(self):
-        """Disappear and let all actions die."""
-        for pos in self.fields:
-            try: del self.scene.hexmap_overlay[pos]
-            except KeyError: print "no such item in overlay hexmap", pos
-        self.fields = {}
-
-    def on_mouse_release(self, x, y, buttons, modifiers):
-        """Process a mouse event."""
-        hex_pos = coordinate_to_hexgrid(x, y)
-        act = self.fields.get(hex_pos, None)
-        if act is not None:
-            action = act.clicked()
-            return action
-
-class MovementOverlay(object):
-    """An overlay to show where a unit can move.
-
-    >>> # Use the overlay without scene — nonfunctional ⇒ only for testing. 
-    >>> c = CommandOverlay(None)
-
-    TODO: Maybe change the overlay to allow for a hidden, yet already created overlays. Currently self.show creates a new overlay at the given position.
-    """
-    def __init__(self, scene):
-        self.scene = scene
-        self.hex_x = 0
-        self.hex_y = 0
-        #: The fields on which we want to be.
-        self.fields = {}
-        #: The hexes we want to place stuff on, relative to our position.
-
-    @property
-    def is_visible(self):
-        return self.fields
-
-    def show(self, positions):
-        for pos in positions:
-            x, y = hexgrid_to_coordinate(*pos)
-            a = Action(self.scene, x=x, y=y)
-            a.move_to_hex(*pos)
-            self.fields[pos] = a
-            self.scene.hexmap_overlay[pos] = a
-            
-    def draw(self):
-        for action in self.fields.values():
-            action.draw()
-    
-    def hide(self):
-        """Disappear and let all actions die."""
-        for pos in self.fields:
-            try:
-                if self.scene.hexmap_overlay[pos] is self.fields[pos]: 
-                    del self.scene.hexmap_overlay[pos]
-            except KeyError: print "no such item in overlay hexmap", pos
-        self.fields = {}
-
-    def on_mouse_release(self, x, y, buttons, modifiers):
-        """Process a mouse event."""
-        hex_pos = coordinate_to_hexgrid(x, y)
-        act = self.fields.get(hex_pos, None)
-        if act is not None:
-            action = act.clicked()
-            return action
-    
-
 
 ### The Scene itself. 
 
     def all_enemies_of(self, unit):
         """Get all enemies of the unit, regardless of their position."""
         return [c for c in self.hexmap.values() if self.are_enemies(unit, c)]
-                
+
+    def switch_to_player_turn(self, char):
+        """Pass control to the player."""
+        self.state["player"] = "player"
+        self.selected = char
+        # show the movement overlay
+        positions = self.selected.movement_targets()
+        self.movement_overlay.show(positions)
+        # simulate a mouse motion to make sure that the selected is shown at the correct position.
+        x, y = self.core.win._mouse_x, self.core.win._mouse_y
+        self.on_mouse_motion(x, y, 0, 0)
+
+
+    def computer_turn(self):
+        """Do computer turns until it’s time for the player to act again."""
+        # game end condition
+        teams = set([c.team for c in self.hexmap.values()])
+        if len(teams) == 1:
+            print "Game finished."
+            print list(teams)[0], "won"
+            print
+            if not self.guide in self.overlay:
+                self.guide.text = "Game finished. " + list(teams)[0] + " won. < Escape to quit >."
+                self.overlay.append(self.guide)
+            return
+
+        next_char = self.next_by_initiative()
+        if next_char.team == self.player_team:
+            return self.switch_to_player_turn(next_char)
+            
+        target = next_char.best_target_hex()
+        next_char.move_to_hex(*target)
+        next_char.attack_best_target_if_possible()
+        self._initiative_step_current()
+        
+    def turn_finished_selected(self):
+        """The turn of the selected unit is finished. Adjust initiative and get the next unit."""
+        self.selected = None
+        # unit acted ⇒ init - 12
+        self._initiative_step_current()
+        # let the computer act
+        self.state["player"] = "CPU"
+
+    def _initiative_step_current(self, amount=12):
+        self.chars_by_initiative[-1][0] -= amount
+        self.chars_by_initiative.sort()
+
+    def pickup_unit(self, unit):
+        """Select a unit"""
+        self.selected = unit
+        self.selected.last_position = (self.selected.hex_x, self.selected.hex_y)
+
+    def drop_selected_unit(self, hex_x, hex_y):
+        """drop the selected unit unto a new place."""
+        grid_unit = self.hexmap.get((hex_x, hex_y), None)
+        if grid_unit is None or grid_unit is self.selected:
+            self.movement_overlay.hide()
+            self.selected.move_to_hex(hex_x, hex_y)
+            actions = self.selected.actions
+            self.command_overlay.show(hex_x, hex_y, actions)
+
+    def postpone_selected_move(self):
+        """Put the selected unit back to its original position and deselect it."""
+        self.selected.move_to_hex(*self.selected.last_position)
+        # adjust the initiative: ini - 6
+        self.chars_by_initiative[-1][0] -= 6
+        self.chars_by_initiative.sort()
+        self.selected = self.next_by_initiative()
+
+               
     # mouse control
     def on_mouse_motion(self, x, y, dx, dy):
         self.blob.x = x
         else:
             self.command_overlay.on_mouse_release(x, y, buttons, modifiers)
 
-    def switch_to_player_turn(self, char):
-        """Pass control to the player."""
-        self.state["player"] = "player"
-        self.selected = char
-        # show the movement overlay
-        positions = self.selected.movement_targets()
-        self.movement_overlay.show(positions)
-        # simulate a mouse motion to make sure that the selected is shown at the correct position.
-        x, y = self.core.win._mouse_x, self.core.win._mouse_y
-        self.on_mouse_motion(x, y, 0, 0)
-
-
-    def computer_turn(self):
-        """Do computer turns until it’s time for the player to act again."""
-        # game end condition
-        teams = set([c.team for c in self.hexmap.values()])
-        if len(teams) == 1:
-            print "Game finished."
-            print list(teams)[0], "won"
-            print
-            if not self.guide in self.overlay:
-                self.guide.text = "Game finished. " + list(teams)[0] + " won. < Escape to quit >."
-                self.overlay.append(self.guide)
-            return
-
-        next_char = self.next_by_initiative()
-        if next_char.team == self.player_team:
-            return self.switch_to_player_turn(next_char)
-            
-        target = next_char.best_target_hex()
-        next_char.move_to_hex(*target)
-        next_char.attack_best_target_if_possible()
-        self._initiative_step_current()
-        
-    def turn_finished_selected(self):
-        """The turn of the selected unit is finished. Adjust initiative and get the next unit."""
-        self.selected = None
-        # unit acted ⇒ init - 12
-        self._initiative_step_current()
-        # let the computer act
-        self.state["player"] = "CPU"
-
-    def _initiative_step_current(self, amount=12):
-        self.chars_by_initiative[-1][0] -= amount
-        self.chars_by_initiative.sort()
-
-    def pickup_unit(self, unit):
-        """Select a unit"""
-        self.selected = unit
-        self.selected.last_position = (self.selected.hex_x, self.selected.hex_y)
-
-    def drop_selected_unit(self, hex_x, hex_y):
-        """drop the selected unit unto a new place."""
-        grid_unit = self.hexmap.get((hex_x, hex_y), None)
-        if grid_unit is None or grid_unit is self.selected:
-            self.movement_overlay.hide()
-            self.selected.move_to_hex(hex_x, hex_y)
-            actions = self.selected.actions
-            self.command_overlay.show(hex_x, hex_y, actions)
-
-    def postpone_selected_move(self):
-        """Put the selected unit back to its original position and deselect it."""
-        self.selected.move_to_hex(*self.selected.last_position)
-        # adjust the initiative: ini - 6
-        self.chars_by_initiative[-1][0] -= 6
-        self.chars_by_initiative.sort()
-        self.selected = self.next_by_initiative()
-
     def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
         """Forwarded mouse input."""
         pass

File fungus_hexbattle_hex.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 pyglet.sprite import Sprite as pyglet_sprite
+
+# own stuff
+from rpg_1d6.char import Char
+# hex functions
+from hexgrid import *
+from fungus_core import Sprite
+
+
+class Hexfield(pyglet_sprite):
+    """A simple hexfield element which adheres to the grid.
+
+    TODO: add show_info() which shows an information overlay in the hexfield."""
+    def __init__(self, scene, image_path=join("graphics", "hex-overlay.png"), x=0, y=0, *args, **kwds): 
+        ## Sprite image
+	img = resource.image(image_path)
+        self.scene = scene
+	super(Hexfield, self).__init__(img, x=x, y=y, *args, **kwds)
+
+    def update(self):
+        pass
+
+    def to_grid_position(self, grid_x, grid_y):
+        """Move to a certain place in the grid."""
+        self.x, self.y = hexgrid_to_coordinate(grid_x, grid_y)
+
+    def move(self, x, y):
+        """Move to the place in the grid which contains the coordinate."""
+        grid_x, grid_y = coordinate_to_hexgrid(x, y)
+        self.to_grid_position(grid_x, grid_y)
+
+    def move_to_hex(self, x, y):
+        self.x, self.y = hexgrid_to_coordinate(x, y)
+
+    def show(self):
+        """Become visible."""
+        if not self in self.scene.visible:
+            self.scene.visible.append(self)
+
+    def hide(self):
+        """Become invisible."""
+        try: self.scene.visible.remove(self)
+        except ValueError: pass # not in the visible list.
+
+    def distance(self, sx, sy, tx, ty):
+        """Get the distance between this hex and the target."""
+        return hexgrid_distance(sx, sy, tx, ty)
+       
+    def distance_to(self, target):
+        """Get the distance between this hex and the target."""
+        return self.distance(self.hex_x, self.hex_y, target.hex_x, target_hex_y)
+
+
+if __name__ == "__main__":
+    # there’s only one reason for calling this file: doctests
+    from doctest import testmod
+    testmod()

File fungus_hexbattle_overlays.py

+#!/usr/bin/env python
+# encoding: utf-8
+
+"""Overlay classes 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 pyglet.sprite import Sprite as pyglet_sprite
+
+# own stuff
+# hex functions
+from hexgrid import *
+from fungus_hexbattle_hex import *
+from fungus_core import Sprite
+
+
+class Action(Hexfield):
+    """A simple action for the command overlay"""
+    def __init__(self, scene, action=None, *args, **kwds):
+        if action is None:
+            image_path=join("graphics", "hex-overlay.png")
+        else:
+            image_path=join("graphics", action["image"])
+        super(Action, self).__init__(scene, image_path=image_path,
+                                     *args, **kwds)
+        self.action = action
+        
+    def clicked(self):
+        """The action was clicked: get active and return the next step.
+
+        If we return None, the command wheel is finished."""
+        return self.action
+
+class CommandOverlay(object):
+    """An overlay which prompts the user to click actions.
+
+    >>> # Use the overlay without scene — nonfunctional ⇒ only for testing. 
+    >>> c = CommandOverlay(None)
+
+    TODO: Maybe change the overlay to allow for a hidden, yet already created overlays. Currently self.show creates a new overlay at the given position."""
+    def __init__(self, scene):
+        self.scene = scene
+        self.hex_x = 0
+        self.hex_y = 0
+        #: The fields on which we want to be.
+        self.fields = {}
+        #: The hexes we want to place stuff on, relative to our position.
+        self.hex_vectors = hex_vectors
+
+    @property
+    def is_visible(self):
+        return self.fields
+
+    def _prepare_actions(self, actions):
+        """Setup the list of actions"""
+        # hide and clear the current actions.
+        self.hide()
+        # and get new ones.
+        if actions is None:
+            # TODO: Get the default list of actions.
+            actions = [None for i in range(7)]
+        else:
+            actions = actions[:]
+            # first object is cancel.
+            actions.insert(0, None)
+            # undefined actions are None
+            actions = actions + [None for i in range(7-len(actions))]
+        for i in range(len(actions)):
+            hex_x, hex_y = self.hex_vectors[i]
+            hex_x += self.hex_x
+            hex_y += self.hex_y
+            x, y = hexgrid_to_coordinate(hex_x, hex_y)
+            action = Action(self.scene, action=actions[i], x = x, y = y)
+            self.scene.hexmap_overlay[(hex_x, hex_y)] = action
+            self.fields[(hex_x, hex_y)] = action
+
+    def draw(self):
+        for action in self.fields.values():
+            action.draw()
+    
+    def show(self, hex_x, hex_y, actions=None):
+        """Create and show the command overlay at the given position. If it gets actions, use these."""
+        self.hex_x = hex_x
+        self.hex_y = hex_y
+        cmds = self._prepare_actions(actions)
+
+    def hide(self):
+        """Disappear and let all actions die."""
+        for pos in self.fields:
+            try: del self.scene.hexmap_overlay[pos]
+            except KeyError: print "no such item in overlay hexmap", pos
+        self.fields = {}
+
+    def on_mouse_release(self, x, y, buttons, modifiers):
+        """Process a mouse event."""
+        hex_pos = coordinate_to_hexgrid(x, y)
+        act = self.fields.get(hex_pos, None)
+        if act is not None:
+            action = act.clicked()
+            return action
+
+class MovementOverlay(object):
+    """An overlay to show where a unit can move.
+
+    >>> # Use the overlay without scene — nonfunctional ⇒ only for testing. 
+    >>> c = CommandOverlay(None)
+
+    TODO: Maybe change the overlay to allow for a hidden, yet already created overlays. Currently self.show creates a new overlay at the given position.
+    """
+    def __init__(self, scene):
+        self.scene = scene
+        self.hex_x = 0
+        self.hex_y = 0
+        #: The fields on which we want to be.
+        self.fields = {}
+        #: The hexes we want to place stuff on, relative to our position.
+
+    @property
+    def is_visible(self):
+        return self.fields
+
+    def show(self, positions):
+        for pos in positions:
+            x, y = hexgrid_to_coordinate(*pos)
+            a = Action(self.scene, x=x, y=y)
+            a.move_to_hex(*pos)
+            self.fields[pos] = a
+            self.scene.hexmap_overlay[pos] = a
+            
+    def draw(self):
+        for action in self.fields.values():
+            action.draw()
+    
+    def hide(self):
+        """Disappear and let all actions die."""
+        for pos in self.fields:
+            try:
+                if self.scene.hexmap_overlay[pos] is self.fields[pos]: 
+                    del self.scene.hexmap_overlay[pos]
+            except KeyError: print "no such item in overlay hexmap", pos
+        self.fields = {}
+
+    def on_mouse_release(self, x, y, buttons, modifiers):
+        """Process a mouse event."""
+        hex_pos = coordinate_to_hexgrid(x, y)
+        act = self.fields.get(hex_pos, None)
+        if act is not None:
+            action = act.clicked()
+            return action
+    
+
+
+if __name__ == "__main__":
+    # there’s only one reason for calling this file: doctests
+    from doctest import testmod
+    testmod()

File fungus_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 pyglet.sprite import Sprite as pyglet_sprite
+
+# own stuff
+from rpg_1d6.char import Char
+# hex functions
+from hexgrid import *
+from fungus_core import Sprite
+
+
+class Hexfield(pyglet_sprite):
+    """A simple hexfield element which adheres to the grid.
+
+    TODO: add show_info() which shows an information overlay in the hexfield."""
+    def __init__(self, scene, image_path=join("graphics", "hex-overlay.png"), x=0, y=0, *args, **kwds): 
+        ## Sprite image
+	img = resource.image(image_path)
+        self.scene = scene
+	super(Hexfield, self).__init__(img, x=x, y=y, *args, **kwds)
+
+    def update(self):
+        pass
+
+    def to_grid_position(self, grid_x, grid_y):
+        """Move to a certain place in the grid."""
+        self.x, self.y = hexgrid_to_coordinate(grid_x, grid_y)
+
+    def move(self, x, y):
+        """Move to the place in the grid which contains the coordinate."""
+        grid_x, grid_y = coordinate_to_hexgrid(x, y)
+        self.to_grid_position(grid_x, grid_y)
+
+    def move_to_hex(self, x, y):
+        self.x, self.y = hexgrid_to_coordinate(x, y)
+
+    def show(self):
+        """Become visible."""
+        if not self in self.scene.visible:
+            self.scene.visible.append(self)
+
+    def hide(self):
+        """Become invisible."""
+        try: self.scene.visible.remove(self)
+        except ValueError: pass # not in the visible list.
+
+    def distance(self, sx, sy, tx, ty):
+        """Get the distance between this hex and the target."""
+        return hexgrid_distance(sx, sy, tx, ty)
+       
+    def distance_to(self, target):
+        """Get the distance between this hex and the target."""
+        return self.distance(self.hex_x, self.hex_y, target.hex_x, target_hex_y)
+
+
+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("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 = self.scene.core.sprite(join("wesnoth", "ball-magenta-small.png"))
+        else: 
+            wound = self.scene.core.sprite(join("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("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()
     (1, 0), # top right
     (1, -1) # bottom right
 ]
+
+if __name__ == "__main__":
+    # there’s only one reason for calling this file: doctests
+    from doctest import testmod
+    testmod()
+