Source

hexbattle / hexbattle_logic.py

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

"""The hexbattle game logic.

It is completely based on hexmaps and should be(-come) mostly
independent of the type of user interaction and presentation.
"""

from core import core, Sprite, run
from os.path import join
from pyglet.clock import schedule_interval, schedule_once
# hex functions
from hexgrid import *

# characters and overlays
from hexbattle_units import *


class Result:
    def __init__(self, name, data=None, description=None, commands=[]):
        """The result of the action."""
        self.name = name
        self.data = data
        self.description = description
        self.commands = commands
        # often used case of having only one command.
        if len(commands) == 1:
            self.command = commands[0]
        else:
            self.command = None
    def __repr__(self):
        return self.__class__.__name__ + "(\"" + self.name + "\", data=" + str(self.data) + ", description=" + str(self.description) + ", commands=" + str(self.commands) + ")"

class Command:
    def __init__(self, fun, name, **arguments):
        """A single command with a function, a name as identifier and
        arguments with the possible values in a tuple."""
        self.fun = fun
        self.name = name
        self.arguments = arguments
    def __call__(self, *args, **kwds): # the args are only there to catch dt=… from schedule.
        return self.fun(**kwds)
    def __repr__(self):
        return self.__class__.__name__ + "(\"" + repr(self.fun) + "\", " + str(self.name) + ",".join(self.arguments) + ")"

class TerrainTile:
    """A tile of ground with properties."""
    def __init__(self, imagepath):
        self.image = imagepath

class Model:
    """The basic game logic. It only knows about hexfields."""
    def __init__(self):
        self.hex_vectors = hex_vectors
        #: Tiles of the ground with properties: {hex-vector: TerrainTile()}
        self.terrain = {}
        #: the results of the current action
        self.results = None
        #: The possible level positions. 
        self.hexmap = {}
        #: stuff which should be shown by a Ui.
        self.visible = []
        #: The current phase of the game.
        self.phase = {}
        # Player States: CPU (computer decision), wait (animation), setup (setup of the scene, noninteractive).
        self.phase["player"] = "setup"
        #: All units that can act
        self.chars = []
        
        # settings
        self.player_team = "trees"
        self.num_rats = 10
        self.num_goblins = 7
        
    
    def are_enemies(self, unit, other):
        """Check if the given units are enemies."""
        return unit.team is None or other.team is None or not unit.team == other.team
    
    def units_around_hex(self, x, y):
        """Return the units around the hexfield."""
        units = []
        for hex_x, hex_y in self.hex_vectors[1:]:
            other = self.hexmap.get((hex_x + x, hex_y + y), None)
            if other is not None: 
                units.append(other)
        return units
    
    def enemies_around(self, unit):
        """Return the enemies which are on hexes touching the hex of the unit (hexfield).
        
        @param unit: A Charakter with at least the attributes group, hex_x and hex_y."""
        enemies = []
        for other in self.units_around_hex(unit.hex_x, unit.hex_y): 
            if other is not None and self.are_enemies(unit, other): 
                enemies.append(other)
        return enemies
    
    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 find_free_hex(self, start_x, start_y):
        """Find the next free hexfield."""
        x, y = start_x, start_y
        vectors = set(self.hex_vectors)
        #: make sure only the new vector parts are tested.
        new_vectors = vectors.copy()
        while True:
            for dx, dy in new_vectors:
                pos = (x+dx, y+dy)
                if not pos in self.hexmap or self.hexmap[pos] is None:
                    return pos
            new_vectors_tmp = set()
            # add vectors around the new vectors new vectors
            for dx, dy in self.hex_vectors:
                for ddx, ddy in new_vectors: 
                    # never check hexes twice
                    pos = (dx+ddx, dy+ddy)
                    if not pos in vectors: 
                        vectors.add(pos)
                        new_vectors_tmp.add(pos)
            new_vectors = new_vectors_tmp
                    
    def setup_playerteam(self):
        for i in range(1):
            x, y = self.find_free_hex(2, 3)
            char = Character(self, self.hexmap)
            char.move_to_hex(x, y)
            char.team = "trees"
            char.show()
            char.attack = 18
            self.chars.append(char)
    
    def setup_step(self, dt=0):
        """setup the level."""
        # if we do not have characters yet, we add all of them.
        if not self.chars:
            self.setup_playerteam()
        # TODO: Make loading a character from a file actually work.
        #with open(join("data", "units", "rats.yml")) as f: 
                  #rat_data = f.read()
        # add rats
        if self.num_rats > 0:
            x, y = self.find_free_hex(7, 1)
            char = Character(self, self.hexmap, image_path = join("graphics", "wesnoth", "giant-rat.png"), source = "tag:1w6.org,2010:Rat")#, template_data = rat_data)
            char.move_to_hex(x, y)
            # we are the rats!
            char.team = "rats"
            char.show()
            char.attack = 6
            self.chars.append(char)
            self.num_rats -= 1
        
        # and add goblins
        if self.num_goblins > 0:
            x, y = self.find_free_hex(6, -2)
            char = Character(self, self.hexmap, image_path = join("graphics", "wesnoth", "impaler.png"), source = "tag:1w6.org,2010:Rat")#, template_data = rat_data)
            char.move_to_hex(x, y)
            # we are the rats!
            char.team = "goblins"
            char.show()
            char.attack = 9
            self.chars.append(char)
            self.num_goblins -= 1
        # pass the control to the AI
        if self.num_goblins <= 0 and self.num_rats <= 0: 
            # sort by initiative
            self.chars_by_initiative = [[c.roll_initiative(), c] for c in self.chars]
            self.chars_by_initiative.sort()
            self.phase["player"] = "CPU"
        # if we’re not finished yet, stay in the setup step
        # TODO: Do this in steps be returning a result which only allows one action: next step.
        else: schedule_once(self.setup_step, 0.02)
        
    
    def next_by_initiative(self):
        """Get the char with the highest initiative, reroll if needed."""
        # reroll while all are below 6. 
        while self.chars_by_initiative[-1][0] < 6:
            # increase by at least 1, else this can go into deadlocks.
            self.chars_by_initiative = [[ini + max(1, c.roll_initiative()/3), c] for ini, c in self.chars_by_initiative]
            self.chars_by_initiative.sort()
        # remove dead or inactive chars
        self.chars_by_initiative = [[ini, c] for ini, c in self.chars_by_initiative if c.active and c.alive]
        # return the char
        return self.chars_by_initiative[-1][1]
    
    def _initiative_step_current(self, amount=12):
        self.chars_by_initiative[-1][0] -= amount
        self.chars_by_initiative.sort()
    
    def switch_to_cpu_turn(self, dt=0):
        self.phase["player"] = "CPU"
    def turn_finished(self):
        # unit acted ⇒ reduce init
        self._initiative_step_current()
        # let the computer act
        self.switch_to_cpu_turn()

    def computer_action(self, char):
        """Let the selected char act once."""
        target = char.best_target_hex()
        char.move_to_hex(*target)
        char.attack_best_target_if_possible()
        self._initiative_step_current()
        
    def computer_turn(self):
        """Let the computer act once."""
        # game end condition
        teams = set([c.team for c in self.hexmap.values()])
        # if someone won, nothing more to be done.
        if len(teams) == 1:
            data = {"winner": list(teams)[0]}
            self.phase["player"] = None
            return Result("finished", data)

        # if it’t the players turn, let him act.
        next_char = self.next_by_initiative()
        if next_char.team == self.player_team:
            # TODO: return the function to select an action for the char instead.
            self.phase["player"] = "player"
            return Result("player_turn", data=next_char, 
                          description="the player character who can act now")

        # otherwise battle on :)
        self.computer_action(next_char)
        self.phase["player"] = "wait"
        # TODO: return the result of the action instead. Or at least a list of changed characters. Best a list of all characters who changed state and how they changed state.
        return Result("step", commands=[Command(self.switch_to_cpu_turn, "continue")])

class Logic:
    def __init__(self):
        self.model = Model()
        #: The scene to switch to at the next possible step.
        self.switch = False 

    def update(self):
        # Update the visible representation of the char.
        for char in self.model.chars:
            char.update_model()
        # do the computer actions. As soon as it’s the players turn, computer_turn() changes the state.
        if self.model.phase["player"] == "setup":
            return Result("init", description="initialized the logic. Setup needed.",
                          commands=[Command(self.model.setup_step, "setup")])
        elif self.model.phase["player"] == "CPU":
            return Result("computer turn", commands=[Command(self.model.computer_turn, "computer turn")])