Source

hexbattle / hexbattle_units.py

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
#!/usr/bin/env python
# encoding: utf-8

"""Unit definitions for hexbattle models."""

# 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: 1. Split methods which mix model and GUI.
    TODO: 2. Add GUI methods from the GUI: The selected GUI gives the chars the capabilities they need. Drawback: Can’t change the UI without reloading.

    >>> # Use the Character without model 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, model, 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__(model, 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 model, as it deals with relationship between units.
        
        FIXME: Some fields are left out for some reason. 
        """
        targets = set()
        def add(position):
            if not position in self.hexmap:
                targets.add(position)
        # 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):
                pos = (self.hex_x + x, self.hex_y + y)
                if pos in targets:
                    continue
                z = - (x+y)
                dist = self.distance(self.hex_x, self.hex_y, self.hex_x + x, self.hex_y + y)
                if dist <= self.max_move:
                    # don’t list those places which are already taken.
                    hexunit = self.hexmap.get(pos, None)
                    if hexunit is self:
                        targets.add(pos)
                    if hexunit is None: 
                        path = self.path_to(pos)
                        if not path: # target not reachable in any way.
                            continue
                        for pos in path[:self.max_move]:
                            add(pos)
                        # if we can reach the target, add it
                        pos_is_reachable = len(path) < self.max_move
                        if pos_is_reachable:
                            add(pos)
        
        return list(targets)

    def twopos2fourhex(self, pos1, pos2):
            return pos1[0], pos1[1], pos2[0], pos2[1]
    def free_neighbor_nodes(self, pos):
            return [(h[0] + pos[0], h[1] + pos[1]) for h in hex_vectors[1:] if self.hexmap.get((h[0] + pos[0], h[1] + pos[1]), None) is None]
    def friendly_neighbor_nodes(self, pos):
        """All nodes over which we can move."""
        targets = []
        for h in hex_vectors[1:]:
            x, y = h[0] + pos[0], h[1] + pos[1]
            unit = self.hexmap.get((x, y), None)
            if unit is None or not self.model.are_enemies(self, unit):
                targets.append((x, y))
        return targets

    def dist_heuristic(self, pos1, pos2):
            """The estimated distance between two nodes."""
            return self.distance(*self.twopos2fourhex(pos1, pos2))

    def path_to(self, goal):
        """Find the cheapest path towards the goal hex.

        TODO: interleaf with movement targets to reduce the cost.
        
        :return: the list of hexes to traverse or None (if no path could be found).
        """
        #: TODO: replace by weighted distances which allow different terrains to have different real costs between neighboring nodes.
        dist = self.dist_heuristic
        def reconstruct(previous, current):
            if current in previous:
                p = reconstruct(previous, previous[current])
                return p + (current, )
            return (current, )
        # TODO: implement path planning
        closed = set()
        start = (self.hex_x, self.hex_y)
        # TODO: use a heapq
        openset = set([start])
        #: node before the current one
        previous = {}
        #: Known shortest distance to each node
        g = {}
        g[start] = 0
        #: Estimate of the distance between two nodes
        h = {}
        h[start] = dist(start, goal)
        #: Combined distance
        f = {}
        f[start] = g[start] + h[start]
        def fval(pos):
            return f[pos]

        while openset:
            current = sorted(openset, key=fval)[0]
            if current == goal:
                return reconstruct(previous, previous[goal])
            openset.remove(current)
            closed.add(current)
            for neighbor in self.friendly_neighbor_nodes(current):
                if neighbor in closed: continue
                # Avoid an infinite loop when trying to find a path to an unreachable hex.
                if self.dist_heuristic(start, neighbor) > self.max_move: 
                    continue # don’t ever check further than we can go
                g_tentative = g[current] + dist(current, neighbor)
                if not neighbor in openset:
                    openset.add(neighbor)
                    h[neighbor] = self.dist_heuristic(neighbor, goal)
                    tentative_is_better = True
                elif g_tentative < g[neighbor]:
                    tentative_is_better = True
                else:
                    tentative_is_better = False
                
                if tentative_is_better:
                    previous[neighbor] = current
                    g[neighbor] = g_tentative
                    f[neighbor] = g[neighbor] + h[neighbor]
        
        return None # explicit fail

    def _guess_damage_against(self, target):
        """Guess the damage we will do against a given target."""
        target_malus = 3 * len(self.model.enemies_around(target)) + target.wounds[0]*3 + target.wounds[1]*6 - 3
        my_malus = 3 * len(self.model.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 = self.model.enemies_around(self)
        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.model.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 model, 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.model.all_enemies_of(self)
            # if we have no enemies, just don’t move
            if not enemies:
                return self.hex_x, self.hex_y
            # otherwise find the possible target which is closest to an enemy
            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.model.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 models 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.model.enemies_around(self)) - 3
        target_malus = 3 * len(self.model.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.model.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_model(self):
        """Update the model representation."""
        if not self.alive:
            self.hide()
            self.remove_from_hexmap()

    def update_visible(self):
        """Update the visible elements, but don’t change the model
        representation (hexmay stuff)."""
        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()
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.