Source

hexbattle / hexbattle_gui.py

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

"""The hexbattle GUI."""

from core import Sprite, core
from hexbattle_overlays import *
from pyglet.window import key
from pyglet.clock import schedule_interval, schedule_once

### Things needed for the scene

IMAGE_BLOB = join("graphics", "blob.png")
IMAGE_TILE = join("graphics", "wesnoth", "deciduous-fall-tile.png")
IMAGE_TILE_LIST = [join("wesnoth", i) for i in [
    #"sand-rock1.png", "sand-rock2.png", "sand-rock3.png", "sand.png",
    "desert-hills.png", "desert-hills3.png", "desert-mountain-peak2.png", "desert-mountains.png", "desert-tile.png", "desert2.png", "desert4.png", "desert6.png", "desert8.png", 
    "desert-hills2.png", "desert-mountain-peak1.png", "desert-mountain-peak3.png", "desert-oasis.png", "desert.png", "desert3.png", "desert5.png", "desert7.png"
    ]]

IMAGE_TILE_SNOW = [join("graphics", "wesnoth", i) for i in ["deciduous-fall-tile.png",
                                                "deciduous-winter-tile.png"]] # "forested-deciduous-summer-hills-tile.png", "forested-hills-tile.png"

class UiState: 
    """The Hexbattle Scene.
    
    The UiState has the following roles:
    
    * UiState sends commands to the logic and reads results. 
    
    Old tasks (need to me moved into the game logic):
    
    * It creates new things.
    * It lets things coordinate, for example via the scene.hexmap dictionary.
    * It manages the relationship between things, for example via the method scene.enemies_around(thing).
    * It serves events to the things.
    * It manages the information, what the user can click and see. The objects change their visible and clickable states themselves. 
    * Every thing knows the scene (has it as self.scene attribute). 
        
    >>> # start the scene with a fake core for testing
    >>> from fungus_core import Core
    >>> l = Logic()
    >>> s = UiState(l)
    """
    def __init__(self, logic, *args, **kwds): 
        """Initialize the scene with a core object for basic functions."""
        # set the background color to white
        gl.glClearColor(255, 255, 255, 0)
        
        #: The Game logic.
        self.logic = logic
        #: the core provides data persistence between scenes.
        self.core = core
        #: The logic tells us what should be visible
        self.visible = self.logic.model.visible
        #: Overlay sprites. They are above all other sprites (included for convenience, since most games need an overlay of sorts)
        self.overlay = []
        #: We still need the hexmap overlay here. The logic does not have to know how an overlay looks.
        self.hexmap_overlay = {}
        # the background
        self.terrain = self.logic.model.terrain        

        self.commands = []
        
        # a hexgrid
        for i in range(-1, 30):
            for j in range(-15, 15):
                # only the visible ones: 
                if j + 0.5*i < -1 or j + 0.5*i > 15:
                    continue
                x, y = hexgrid_to_coordinate(i, j)
                im = choice(IMAGE_TILE_LIST)
                blob = Hexfield(self, image_path=join("graphics", im), x=x, y=y)
                self.terrain[(i,j)] = blob
	
	self.hexfield = Hexfield(self)
        self.hexfield.show()
        self.blob = Sprite(IMAGE_BLOB, x=212, y=208)
        self.visible.append(self.blob)
        
        self.selected = None
        self.command_overlay = CommandOverlay(self)
        self.movement_overlay = MovementOverlay(self)
        self.overlay.append(self.command_overlay)
        self.overlay.append(self.movement_overlay)
        
        self.theater_overlay = TheaterOverlay(self)
        self.overlay.append(self.theater_overlay)
        self.theater_overlay.show(join("graphics", "wesnoth", "druid.png"))
        self.theater_overlay.show(join("graphics", "wesnoth", "druid.png"),position="right")

        self.text_overlay = TextOverlay(self)
        self.text_overlay.show("""
Select a white hex to move
Click the sword to attack
Select the enemy to strike
             
     Kill all enemies.     
  Keep your units together.
  Don't get surrounded.    
     < enter to hide >     """)
        self.overlay.append(self.text_overlay)

    def update(self, dt=0): 
        """Update the stats of all scene objects. 
        
Don't blit them, though. That's done by the Presentation.
        
To show something, add it to the self.visible list. 
To add a collider, add it to the self.colliding list. 
To add an overlay sprite, add it to the self.overlay list. 
"""	
        # Update the visible representation of the char.
        for char in self.logic.model.chars:
            char.update_visible()
        
        # Update the model.
        # TODO: Just execute a step action, when the model tells us
        # that we can do so.
        # TODO: Add an inactive state: There’s nothing we can do
        # (action already scheduled).
        result = self.logic.update()

        if not result: return # inactive state. TODO: add an explicit result.
        
        if result.name == "init":
            return schedule_once(result.command, 0.01)
        
        if result.name == "computer turn":
            res = result.command()
            return self.eval_computer_turn(res)
            
    def update_interactive_elements(self, dt=0): 
        """Update the interactive elements so they stay fast at all times."""	
        self.blob.update()
    
    def switch_to_player_turn(self, char):
        """Pass control to the player."""
        self.selected = char
        # show the movement overlay
        positions = self.selected.movement_targets()
        self.movement_overlay.show(positions)
    
    def wait(self, delay=0.3):
        """wait for delay seconds. Then hand over to CPU turn."""
        schedule_once(self.logic.model.switch_to_cpu_turn, delay)
    
    def eval_computer_turn(self, result):
        """Do computer turns until it’s time for the player to act again."""
        # game end condition
        if result.name == "finished":
            self.text_overlay.show("Game finished. \n" + result.data["winner"] + " won \n< Escape to quit >.")
            return
        if result.name == "player_turn":
            # TODO: refactor, so the Model actually tells the GUI what it has to decide now: What does this char do now? Return the list of commands the movement overlay can call.
            return self.switch_to_player_turn(char=result.data)
        if result.name == "step":
            # wait a moment so the user can see what happened.
            # step only has one option: continue
            # this is too strong coupling for my taste, but I don’t know yet how to do it better…
            return schedule_once(result.command, 0.1)
                
    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.logic.model.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 turn_finished_selected(self):
        """The turn of the selected unit is finished. Adjust initiative and get the next unit."""
        self.selected = None
        self.logic.model.turn_finished()

    def click(self, x, y): 
        """Forwarded mouse input."""
        hex_x, hex_y = coordinate_to_hexgrid(x, y)
        # TODO: move if/else into a state-machine in the UiState: Check if the action fits one of the possible commands.
        # if we’re dragging something, we act on that. 
        if self.selected is not None:
            # we only move the item and show the commands, if there’s no overlay. 
            if not self.command_overlay.is_visible:
                if self.movement_overlay.fields.get((hex_x, hex_y), None) is not None: 
                    self.drop_selected_unit(hex_x, hex_y)
            # if there’s already an overlay, we check if we resolved an item.
            else:
                action = self.command_overlay.on_mouse_release(x, y)
                if action is not None:
                    # global (UI) actions
                    if action == "abort move":
                        self.abort_selected_move()
                        self.command_overlay.hide()
                        self.turn_finished_selected()
                    # actions by the unit
                    actions = self.selected.act(action)
                else:
                    actions = None
                # if we get new actions, let them appear around the unit.
                if actions is not None:
                    self.command_overlay.show(self.selected.hex_x,
                                              self.selected.hex_y,
                                              actions)
                else:
                    # unit turn finished
                    self.command_overlay.hide()
                    self.turn_finished_selected()
        elif not self.command_overlay.is_visible and self.logic.model.hexmap.get((hex_x, hex_y), None) is not None:
            pass # no action. All selecting is done by the computer.
        else:
            self.command_overlay.on_mouse_release(x, y)

    def move_active(self, x, y):
        """Move the elements which follow the mouse."""
        self.blob.x, self.blob.y = x, y
        self.hexfield.move(x, y)
        if self.selected is not None and not self.command_overlay.is_visible:
            self.selected.move(x, y)
        self.hexfield.move(x, y)
        

class Presentation:
    """The presentation shows the UiState to the user."""
    def __init__(self, state):
        self.state = state
        self.sprites = {}
    def add_sprite(self, thing):
        s = Sprite(thing.image_path, x=0,y=0)
        self.sprites[thing] = s
        return s
    def draw(self):
        # TODO: UiState should deliver one list of lists of stuff to show.
        #       Better: One list which contains lists (ordered) or sets (unordered).
        #       Stuff on which I can call draw()
        visible = self.state.terrain.values() + self.state.visible
        for i in visible:
            i.draw()
            continue
            # TODO: Move sprite creation into the UiState. Remove the
            # following part of the loop.

            # one sprite per object, but sharing images.
            try:
                s = self.sprites[i]
            except KeyError:
                s = self.add_sprite(i)
            s.x, s.y = i.x, i.y
            s.draw()
        # TODO: Find a way to draw text.
        #       UiState.text?
        #       Presentation should just draw the UiState and not
        #       manage state itself.
        for over in self.state.overlay:
                over.draw()

class Input:
    """Take all User Input."""
    def __init__(self, state):
        self.state = state
    # mouse control
    def on_mouse_motion(self, x, y, dx, dy):
        self.state.move_active(x, y)
    
    def on_mouse_press(self, x, y, buttons, modifiers): 
        """Forwarded mouse input."""
        hex_pos = coordinate_to_hexgrid(x, y)
        # do nothing, if we hit an overlay field: Hides units below.
        if self.state.command_overlay.is_visible:
            return
    
    def on_mouse_drag(self, x, y, dx, dy, buttons, modifiers): 
        """Forwarded mouse input."""
        if self.state.selected is not None and not self.state.command_overlay.is_visible:
            self.state.selected.move(x, y)
        self.state.hexfield.move(x, y)
    
    def on_mouse_release(self, x, y, buttons, modifiers): 
        """Forwarded mouse input."""
        self.state.click(x, y)
    
    def on_mouse_scroll(self, x, y, scroll_x, scroll_y):
        """Forwarded mouse input."""
        pass
    
    def on_mouse_enter(self, x, y):
        """Forwarded mouse input."""
        pass
    
    def on_mouse_leave(self, x, y):
        """Forwarded mouse input."""
        pass
    
    def on_key_press(self, symbol, modifiers):
        """Forwarded keyboard input."""
        if symbol == key.ENTER or symbol == key.RETURN:
            self.state.text_overlay.hide()
    
class Interaction:
    def __init__(self, logic):
        self.state = UiState(logic)
        self.input = Input(self.state)
        self.presentation = Presentation(self.state)
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.