diff options
author | Philip Paquette <pcpaquette@gmail.com> | 2018-09-26 07:48:55 -0400 |
---|---|---|
committer | Philip Paquette <pcpaquette@gmail.com> | 2019-04-18 11:14:24 -0400 |
commit | 6187faf20384b0c5a4966343b2d4ca47f8b11e45 (patch) | |
tree | 151ccd21aea20180432c13fe4b58240d3d9e98b6 /diplomacy/engine/renderer.py | |
parent | 96b7e2c03ed98705754f13ae8efa808b948ee3a8 (diff) |
Release v1.0.0 - Diplomacy Game Engine - AGPL v3+ License
Diffstat (limited to 'diplomacy/engine/renderer.py')
-rw-r--r-- | diplomacy/engine/renderer.py | 789 |
1 files changed, 789 insertions, 0 deletions
diff --git a/diplomacy/engine/renderer.py b/diplomacy/engine/renderer.py new file mode 100644 index 0000000..8b5e10c --- /dev/null +++ b/diplomacy/engine/renderer.py @@ -0,0 +1,789 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +# -*- coding: utf-8 -*- +""" Renderer + - Contains the renderer object which is responsible for rendering a game state to svg +""" +import os +from xml.dom import minidom +from diplomacy import settings + +# Constants +LAYER_SC = 'SupplyCenterLayer' +LAYER_ORDER = 'OrderLayer' +LAYER_UNIT = 'UnitLayer' +LAYER_DISL = 'DislodgedUnitLayer' +ARMY = 'Army' +FLEET = 'Fleet' + +def _attr(node_element, attr_name): + """ Shorthand method to retrieve an XML attribute """ + return node_element.attributes[attr_name].value + +def _offset(str_float, offset): + """ Shorthand to add a offset to an attribute """ + return str(float(str_float) + offset) + + +class Renderer(): + """ Renderer object responsible for rendering a game state to svg """ + + def __init__(self, game): + """ Constructor + :param game: The instantiated game object to render + :type game: diplomacy.Game + """ + self.game = game + self.metadata = {} + self.xml_map = None + self.xml_map_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', self.game.map.name + '.svg') + + # Loading XML + if os.path.exists(self.xml_map_path): + self.xml_map = minidom.parse(self.xml_map_path).toxml() + self._load_metadata() + + def norm_order(self, order): + """ Normalizes the order format and split it into tokens + This is only used for **movement** orders (to make sure NO_CHECK games used the correct format) + Formats: + A PAR H + A PAR - BUR [VIA] + A PAR S BUR + A PAR S F BRE - PIC + F BRE C A PAR - LON + + :param order: The unformatted order (e.g. 'Paris - Burgundy') + :return: The tokens of the formatted order (e.g. ['A', 'PAR', '-', 'BUR']) + """ + return self.game._add_unit_types(self.game._expand_order(order.split())) # pylint: disable=protected-access + + def render(self, incl_orders=True, incl_abbrev=False, output_format='svg'): + """ Renders the current game and returns the XML representation + :param incl_orders: Optional. Flag to indicate we also want to render orders. + :param incl_abbrev: Optional. Flag to indicate we also want to display the provinces abbreviations. + :param output_format: The desired output format. + :return: The rendered image in the specified format. + """ + # pylint: disable=too-many-branches + if output_format not in ['svg']: + raise ValueError('Only "svg" format is current supported.') + if not self.game or not self.game.map or not self.xml_map: + return None + + # Parsing XML + xml_map = minidom.parseString(self.xml_map) + scs = self.game.map.scs[:] + + # Setting phase and note + nb_centers = [(power.name[:3], len(power.centers)) + for power in self.game.powers.values() + if not power.is_eliminated()] + nb_centers = sorted(nb_centers, key=lambda key: key[1], reverse=True) + nb_centers_per_power = ' '.join(['{}: {}'.format(name, centers) for name, centers in nb_centers]) + xml_map = self._set_current_phase(xml_map, self.game.get_current_phase()) + xml_map = self._set_note(xml_map, nb_centers_per_power, self.game.note) + + # Adding units, supply centers, and influence + for power in self.game.powers.values(): + for unit in power.units: + xml_map = self._add_unit(xml_map, unit, power.name, is_dislodged=False) + for unit in power.retreats: + xml_map = self._add_unit(xml_map, unit, power.name, is_dislodged=True) + for center in power.centers: + xml_map = self._add_supply_center(xml_map, center, power.name) + xml_map = self._set_influence(xml_map, center, power.name, has_supply_center=True) + scs.remove(center) + for loc in power.influence: + xml_map = self._set_influence(xml_map, loc, power.name, has_supply_center=False) + + # Orders + if incl_orders: + + # Regular orders (Normalized) + # A PAR H + # A PAR - BUR [VIA] + # A PAR S BUR + # A PAR S F BRE - PIC + # F BRE C A PAR - LON + for order_key in power.orders: + + # No_check order (Order, Invalid, Reorder) + # Otherwise regular order (unit is key, order without unit is value) + if order_key[0] in 'RIO': + order = power.orders[order_key] + else: + order = '{} {}'.format(order_key, power.orders[order_key]) + + # Normalizing and splitting in tokens + tokens = self.norm_order(order) + unit_loc = tokens[1] + + # Parsing based on order type + if not tokens or len(tokens) < 3: + continue + elif tokens[2] == 'H': + xml_map = self._issue_hold_order(xml_map, unit_loc, power.name) + elif tokens[2] == '-': + dest_loc = tokens[-1] if tokens[-1] != 'VIA' else tokens[-2] + xml_map = self._issue_move_order(xml_map, unit_loc, dest_loc, power.name) + elif tokens[2] == 'S': + dest_loc = tokens[-1] + if '-' in tokens: + src_loc = tokens[4] if tokens[3] == 'A' or tokens[3] == 'F' else tokens[3] + xml_map = self._issue_support_move_order(xml_map, unit_loc, src_loc, dest_loc, power.name) + else: + xml_map = self._issue_support_hold_order(xml_map, unit_loc, dest_loc, power.name) + elif tokens[2] == 'C': + src_loc = tokens[4] if tokens[3] == 'A' or tokens[3] == 'F' else tokens[3] + dest_loc = tokens[-1] + if src_loc != dest_loc and '-' in tokens: + xml_map = self._issue_convoy_order(xml_map, unit_loc, src_loc, dest_loc, power.name) + else: + raise RuntimeError('Unknown order: {}'.format(' '.join(tokens))) + + # Adjustment orders + # VOID xxx + # A PAR B + # A PAR D + # A PAR R BUR + # WAIVE + for order in power.adjust: + tokens = order.split() + if not tokens or tokens[0] == 'VOID' or tokens[-1] == 'WAIVE': + continue + elif tokens[-1] == 'B': + if len(tokens) < 3: + continue + xml_map = self._issue_build_order(xml_map, tokens[0], tokens[1], power.name) + elif tokens[-1] == 'D': + xml_map = self._issue_disband_order(xml_map, tokens[1]) + elif tokens[-2] == 'R': + src_loc = tokens[1] if tokens[0] == 'A' or tokens[0] == 'F' else tokens[0] + dest_loc = tokens[-1] + xml_map = self._issue_move_order(xml_map, src_loc, dest_loc, power.name) + else: + raise RuntimeError('Unknown order: {}'.format(order)) + + # Adding remaining supply centers + for center in scs: + xml_map = self._add_supply_center(xml_map, center, None) + + # Removing abbrev and mouse layer + svg_node = xml_map.getElementsByTagName('svg')[0] + for child_node in svg_node.childNodes: + if child_node.nodeName != 'g': + continue + if _attr(child_node, 'id') == 'BriefLabelLayer' and not incl_abbrev: + svg_node.removeChild(child_node) + elif _attr(child_node, 'id') == 'MouseLayer': + svg_node.removeChild(child_node) + + # Returning + return xml_map.toxml() + + def _load_metadata(self): + """ Loads meta-data embedded in the XML map and clears unused nodes """ + if not self.xml_map: + return + xml_map = minidom.parseString(self.xml_map) + + # Data + self.metadata = { + 'color': {}, + 'symbol_size': {}, + 'orders': {}, + 'coord': {} + } + + # Order drawings + for order_drawing in xml_map.getElementsByTagName('jdipNS:ORDERDRAWING'): + for child_node in order_drawing.childNodes: + + # Power Colors + if child_node.nodeName == 'jdipNS:POWERCOLORS': + for power_color in child_node.childNodes: + if power_color.nodeName == 'jdipNS:POWERCOLOR': + self.metadata['color'][_attr(power_color, 'power').upper()] = _attr(power_color, 'color') + + # Symbol size + elif child_node.nodeName == 'jdipNS:SYMBOLSIZE': + self.metadata['symbol_size'][_attr(child_node, 'name')] = (_attr(child_node, 'height'), + _attr(child_node, 'width')) + + # Order type + elif child_node.nodeName.startswith('jdipNS'): + order_type = child_node.nodeName.replace('jdipNS:', '') + self.metadata['orders'][order_type] = {} + for attr_name, attr_value in child_node.attributes.items(): + if ':' in attr_name: + continue + self.metadata['orders'][order_type][attr_name] = attr_value + + # Object coordinates + for province_data in xml_map.getElementsByTagName('jdipNS:PROVINCE_DATA'): + for child_node in province_data.childNodes: + + # Province + if child_node.nodeName == 'jdipNS:PROVINCE': + province = _attr(child_node, 'name').upper().replace('-', '/') + self.metadata['coord'][province] = {} + + for coord_node in child_node.childNodes: + if coord_node.nodeName == 'jdipNS:UNIT': + self.metadata['coord'][province]['unit'] = (_attr(coord_node, 'x'), _attr(coord_node, 'y')) + elif coord_node.nodeName == 'jdipNS:DISLODGED_UNIT': + self.metadata['coord'][province]['disl'] = (_attr(coord_node, 'x'), _attr(coord_node, 'y')) + elif coord_node.nodeName == 'jdipNS:SUPPLY_CENTER': + self.metadata['coord'][province]['sc'] = (_attr(coord_node, 'x'), _attr(coord_node, 'y')) + + # Deleting + svg_node = xml_map.getElementsByTagName('svg')[0] + svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:DISPLAY')[0]) + svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:ORDERDRAWING')[0]) + svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:PROVINCE_DATA')[0]) + self.xml_map = xml_map.toxml() + + def _add_unit(self, xml_map, unit, power_name, is_dislodged): + """ Adds a unit to the map + :param xml_map: The xml map being generated + :param unit: The unit to add (e.g. 'A PAR') + :param power_name: The name of the power owning the unit (e.g. 'FRANCE') + :param is_dislodged: Boolean. Indicates if the unit is dislodged + :return: Nothing + """ + unit_type, loc = unit.split() + symbol = FLEET if unit_type == 'F' else ARMY + loc_x = _offset(self.metadata['coord'][loc][('unit', 'disl')[is_dislodged]][0], -11.5) + loc_y = _offset(self.metadata['coord'][loc][('unit', 'disl')[is_dislodged]][1], - 10.) + node = xml_map.createElement('use') + node.setAttribute('x', loc_x) + node.setAttribute('y', loc_y) + node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + node.setAttribute('xlink:href', '#{}{}'.format(('', 'Dislodged')[is_dislodged], symbol)) + node.setAttribute('class', 'unit{}'.format(power_name.lower())) + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' \ + and _attr(child_node, 'id') == ['UnitLayer', 'DislodgedUnitLayer'][is_dislodged]: + child_node.appendChild(node) + break + return xml_map + + def _add_supply_center(self, xml_map, loc, power_name): + """ Adds a supply center to the map + :param xml_map: The xml map being generated + :param loc: The province where to add the SC (e.g. 'PAR') + :param power_name: The name of the power owning the SC or None + :return: Nothing + """ + symbol = 'SupplyCenter' + loc_x = _offset(self.metadata['coord'][loc]['sc'][0], -8.5) + loc_y = _offset(self.metadata['coord'][loc]['sc'][1], -11.) + node = xml_map.createElement('use') + node.setAttribute('x', loc_x) + node.setAttribute('y', loc_y) + node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + node.setAttribute('xlink:href', '#{}'.format(symbol)) + if power_name: + node.setAttribute('class', 'sc{}'.format(power_name.lower())) + else: + node.setAttribute('class', 'scnopower') + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'SupplyCenterLayer': + child_node.appendChild(node) + break + return xml_map + + def _set_influence(self, xml_map, loc, power_name, has_supply_center=False): + """ Sets the influence on the map + :param xml_map: The xml map being generated + :param loc: The province being influenced (e.g. 'PAR') + :param power_name: The name of the power influencing the province + :param has_supply_center: Boolean flag to acknowledge we are modifying a loc with a SC + :return: Nothing + """ + loc = loc.upper()[:3] + if loc in self.game.map.scs and not has_supply_center: + return xml_map + if self.game.map.area_type(loc) not in ['LAND', 'COAST']: + return xml_map + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'MapLayer': + for map_node in child_node.childNodes: + if map_node.nodeName == 'path' and _attr(map_node, 'id') == '_{}'.format(loc.lower()): + if power_name: + map_node.setAttribute('class', power_name.lower()) + else: + map_node.setAttribute('class', 'nopower') + return xml_map + + # Returning + return xml_map + + @staticmethod + def _set_current_phase(xml_map, current_phase): + """ Sets the phase text at the bottom right of the the map + :param xml_map: The xml map being generated + :param current_phase: The current phase (e.g. 'S1901M) + :return: Nothing + """ + current_phase = 'FINAL' if current_phase[0] == '?' else current_phase + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'text' and _attr(child_node, 'id') == 'CurrentPhase': + child_node.childNodes[0].nodeValue = current_phase + return xml_map + return xml_map + + @staticmethod + def _set_note(xml_map, note_1, note_2): + """ Sets a note at the top left of the map + :param xml_map: The xml map being generated + :param note_1: The text to display on the first line + :param note_2: The text to display on the second line + :return: Nothing + """ + note_1 = note_1 or ' ' + note_2 = note_2 or ' ' + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'text' and _attr(child_node, 'id') == 'CurrentNote': + child_node.childNodes[0].nodeValue = note_1 + if child_node.nodeName == 'text' and _attr(child_node, 'id') == 'CurrentNote2': + child_node.childNodes[0].nodeValue = note_2 + return xml_map + + def _issue_hold_order(self, xml_map, loc, power_name): + """ Adds a hold order to the map + :param xml_map: The xml map being generated + :param loc: The province where the unit is holding (e.g. 'PAR') + :param power_name: The name of the power owning the unit + :return: Nothing + """ + # Calculating polygon coord + polygon_coord = [] + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 8.5) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 9.5) + for offset in [(13.8, -33.3), (33.3, -13.8), (33.3, 13.8), (13.8, 33.3), (-13.8, 33.3), (-33.3, 13.8), + (-33.3, -13.8), (-13.8, -33.3)]: + polygon_coord += [_offset(loc_x, offset[0]) + ',' + _offset(loc_y, offset[1])] + + # Building polygon + g_node = xml_map.createElement('g') + + poly_1 = xml_map.createElement('polygon') + poly_1.setAttribute('stroke-width', '10') + poly_1.setAttribute('class', 'varwidthshadow') + poly_1.setAttribute('points', ' '.join(polygon_coord)) + + poly_2 = xml_map.createElement('polygon') + poly_2.setAttribute('stroke-width', '6') + poly_2.setAttribute('class', 'varwidthorder') + poly_2.setAttribute('points', ' '.join(polygon_coord)) + poly_2.setAttribute('stroke', self.metadata['color'][power_name]) + + g_node.appendChild(poly_1) + g_node.appendChild(poly_2) + + # Inserting + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer1': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + def _issue_support_hold_order(self, xml_map, loc, dest_loc, power_name): + """ Issues a support hold order + :param xml_map: The xml map being generated + :param loc: The location of the unit sending support (e.g. 'BER') + :param dest_loc: The location where the unit is holding from (e.g. 'PAR') + :param power_name: The power name issuing the move order + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 10) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting destination + delta_x = float(dest_loc_x) - float(loc_x) + delta_y = float(dest_loc_y) - float(loc_y) + vector_length = (delta_x ** 2. + delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(loc_x) + (vector_length - 35.) / vector_length * delta_x, 2)) + dest_loc_y = str(round(float(loc_y) + (vector_length - 35.) / vector_length * delta_y, 2)) + + # Getting polygon coordinates + polygon_coord = [] + poly_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 8.5) + poly_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 9.5) + for offset in [(15.9, -38.3), (38.3, -15.9), (38.3, 15.9), (15.9, 38.3), (-15.9, 38.3), (-38.3, 15.9), + (-38.3, -15.9), (-15.9, -38.3)]: + polygon_coord += [_offset(poly_loc_x, offset[0]) + ',' + _offset(poly_loc_y, offset[1])] + + # Creating nodes + g_node = xml_map.createElement('g') + + shadow_line = xml_map.createElement('line') + shadow_line.setAttribute('x1', loc_x) + shadow_line.setAttribute('y1', loc_y) + shadow_line.setAttribute('x2', dest_loc_x) + shadow_line.setAttribute('y2', dest_loc_y) + shadow_line.setAttribute('class', 'shadowdash') + + support_line = xml_map.createElement('line') + support_line.setAttribute('x1', loc_x) + support_line.setAttribute('y1', loc_y) + support_line.setAttribute('x2', dest_loc_x) + support_line.setAttribute('y2', dest_loc_y) + support_line.setAttribute('class', 'supportorder') + support_line.setAttribute('stroke', self.metadata['color'][power_name]) + + shadow_poly = xml_map.createElement('polygon') + shadow_poly.setAttribute('class', 'shadowdash') + shadow_poly.setAttribute('points', ' '.join(polygon_coord)) + + support_poly = xml_map.createElement('polygon') + support_poly.setAttribute('class', 'supportorder') + support_poly.setAttribute('points', ' '.join(polygon_coord)) + support_poly.setAttribute('stroke', self.metadata['color'][power_name]) + + # Inserting + g_node.appendChild(shadow_line) + g_node.appendChild(support_line) + g_node.appendChild(shadow_poly) + g_node.appendChild(support_poly) + + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer2': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + def _issue_move_order(self, xml_map, src_loc, dest_loc, power_name): + """ Issues a move order + :param xml_map: The xml map being generated + :param src_loc: The location where the unit is moving from (e.g. 'PAR') + :param dest_loc: The location where the unit is moving to (e.g. 'MAR') + :param power_name: The power name issuing the move order + :return: Nothing + """ + if self.game.get_current_phase()[-1] == 'R': + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], -2.5) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], -2.5) + else: + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting destination + delta_x = float(dest_loc_x) - float(src_loc_x) + delta_y = float(dest_loc_y) - float(src_loc_y) + vector_length = (delta_x ** 2. + delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(src_loc_x) + (vector_length - 30.) / vector_length * delta_x, 2)) + dest_loc_y = str(round(float(src_loc_y) + (vector_length - 30.) / vector_length * delta_y, 2)) + + # Creating nodes + g_node = xml_map.createElement('g') + + line_with_shadow = xml_map.createElement('line') + line_with_shadow.setAttribute('x1', src_loc_x) + line_with_shadow.setAttribute('y1', src_loc_y) + line_with_shadow.setAttribute('x2', dest_loc_x) + line_with_shadow.setAttribute('y2', dest_loc_y) + line_with_shadow.setAttribute('class', 'varwidthshadow') + line_with_shadow.setAttribute('stroke-width', '10') + + line_with_arrow = xml_map.createElement('line') + line_with_arrow.setAttribute('x1', src_loc_x) + line_with_arrow.setAttribute('y1', src_loc_y) + line_with_arrow.setAttribute('x2', dest_loc_x) + line_with_arrow.setAttribute('y2', dest_loc_y) + line_with_arrow.setAttribute('class', 'varwidthorder') + line_with_arrow.setAttribute('stroke', self.metadata['color'][power_name]) + line_with_arrow.setAttribute('stroke-width', '6') + line_with_arrow.setAttribute('marker-end', 'url(#arrow)') + + # Inserting + g_node.appendChild(line_with_shadow) + g_node.appendChild(line_with_arrow) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer1': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + def _issue_support_move_order(self, xml_map, loc, src_loc, dest_loc, power_name): + """ Issues a support move order + :param xml_map: The xml map being generated + :param loc: The location of the unit sending support (e.g. 'BER') + :param src_loc: The location where the unit is moving from (e.g. 'PAR') + :param dest_loc: The location where the unit is moving to (e.g. 'MAR') + :param power_name: The power name issuing the move order + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 10) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 10) + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting destination + delta_x = float(dest_loc_x) - float(src_loc_x) + delta_y = float(dest_loc_y) - float(src_loc_y) + vector_length = (delta_x ** 2. + delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(src_loc_x) + (vector_length - 30.) / vector_length * delta_x, 2)) + dest_loc_y = str(round(float(src_loc_y) + (vector_length - 30.) / vector_length * delta_y, 2)) + + # Creating nodes + g_node = xml_map.createElement('g') + + path_with_shadow = xml_map.createElement('path') + path_with_shadow.setAttribute('class', 'shadowdash') + path_with_shadow.setAttribute('d', 'M {},{} C {},{} {},{} {},{}'.format(loc_x, loc_y, + src_loc_x, src_loc_y, + src_loc_x, src_loc_y, + dest_loc_x, dest_loc_y)) + + path_with_arrow = xml_map.createElement('path') + path_with_arrow.setAttribute('class', 'supportorder') + path_with_arrow.setAttribute('stroke', self.metadata['color'][power_name]) + path_with_arrow.setAttribute('marker-end', 'url(#arrow)') + path_with_arrow.setAttribute('d', 'M {},{} C {},{} {},{} {},{}'.format(loc_x, loc_y, + src_loc_x, src_loc_y, + src_loc_x, src_loc_y, + dest_loc_x, dest_loc_y)) + + # Inserting + g_node.appendChild(path_with_shadow) + g_node.appendChild(path_with_arrow) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer2': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + def _issue_convoy_order(self, xml_map, loc, src_loc, dest_loc, power_name): + """ Issues a convoy order + :param xml_map: The xml map being generated + :param loc: The location of the unit convoying (e.g. 'BER') + :param src_loc: The location where the unit being convoyed is moving from (e.g. 'PAR') + :param dest_loc: The location where the unit being convoyed is moving to (e.g. 'MAR') + :param power_name: The power name issuing the convoy order + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], 10) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], 10) + src_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + src_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + dest_loc_x = _offset(self.metadata['coord'][dest_loc]['unit'][0], 10) + dest_loc_y = _offset(self.metadata['coord'][dest_loc]['unit'][1], 10) + + # Adjusting starting arrow (from convoy to start location) + # This is to avoid the end of the arrow conflicting with the convoy triangle + src_delta_x = float(src_loc_x) - float(loc_x) + src_delta_y = float(src_loc_y) - float(loc_y) + src_vector_length = (src_delta_x ** 2. + src_delta_y ** 2.) ** 0.5 + src_loc_x_1 = str(round(float(loc_x) + (src_vector_length - 30.) / src_vector_length * src_delta_x, 2)) + src_loc_y_1 = str(round(float(loc_y) + (src_vector_length - 30.) / src_vector_length * src_delta_y, 2)) + + # Adjusting destination arrow (from start location to destination location) + # This is to avoid the start of the arrow conflicting with the convoy triangle + dest_delta_x = float(src_loc_x) - float(dest_loc_x) + dest_delta_y = float(src_loc_y) - float(dest_loc_y) + dest_vector_length = (dest_delta_x ** 2. + dest_delta_y ** 2.) ** 0.5 + src_loc_x_2 = str(round(float(dest_loc_x) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_x, 2)) + src_loc_y_2 = str(round(float(dest_loc_y) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_y, 2)) + + # Adjusting destination arrow (from start location to destination location) + # This is to avoid the start of the arrow conflicting with the convoy triangle + dest_delta_x = float(dest_loc_x) - float(src_loc_x) + dest_delta_y = float(dest_loc_y) - float(src_loc_y) + dest_vector_length = (dest_delta_x ** 2. + dest_delta_y ** 2.) ** 0.5 + dest_loc_x = str(round(float(src_loc_x) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_x, 2)) + dest_loc_y = str(round(float(src_loc_y) + (dest_vector_length - 30.) / dest_vector_length * dest_delta_y, 2)) + + # Getting convoy triangle coordinates + triangle_coord = [] + triangle_loc_x = _offset(self.metadata['coord'][src_loc]['unit'][0], 10) + triangle_loc_y = _offset(self.metadata['coord'][src_loc]['unit'][1], 10) + for offset in [(0, -38.3), (33.2, 19.1), (-33.2, 19.1)]: + triangle_coord += [_offset(triangle_loc_x, offset[0]) + ',' + _offset(triangle_loc_y, offset[1])] + + # Creating nodes + g_node = xml_map.createElement('g') + + src_shadow_line = xml_map.createElement('line') + src_shadow_line.setAttribute('x1', loc_x) + src_shadow_line.setAttribute('y1', loc_y) + src_shadow_line.setAttribute('x2', src_loc_x_1) + src_shadow_line.setAttribute('y2', src_loc_y_1) + src_shadow_line.setAttribute('class', 'shadowdash') + + dest_shadow_line = xml_map.createElement('line') + dest_shadow_line.setAttribute('x1', src_loc_x_2) + dest_shadow_line.setAttribute('y1', src_loc_y_2) + dest_shadow_line.setAttribute('x2', dest_loc_x) + dest_shadow_line.setAttribute('y2', dest_loc_y) + dest_shadow_line.setAttribute('class', 'shadowdash') + + src_convoy_line = xml_map.createElement('line') + src_convoy_line.setAttribute('x1', loc_x) + src_convoy_line.setAttribute('y1', loc_y) + src_convoy_line.setAttribute('x2', src_loc_x_1) + src_convoy_line.setAttribute('y2', src_loc_y_1) + src_convoy_line.setAttribute('class', 'convoyorder') + src_convoy_line.setAttribute('stroke', self.metadata['color'][power_name]) + + dest_convoy_line = xml_map.createElement('line') + dest_convoy_line.setAttribute('x1', src_loc_x_2) + dest_convoy_line.setAttribute('y1', src_loc_y_2) + dest_convoy_line.setAttribute('x2', dest_loc_x) + dest_convoy_line.setAttribute('y2', dest_loc_y) + dest_convoy_line.setAttribute('class', 'convoyorder') + dest_convoy_line.setAttribute('stroke', self.metadata['color'][power_name]) + dest_convoy_line.setAttribute('marker-end', 'url(#arrow)') + + shadow_poly = xml_map.createElement('polygon') + shadow_poly.setAttribute('class', 'shadowdash') + shadow_poly.setAttribute('points', ' '.join(triangle_coord)) + + convoy_poly = xml_map.createElement('polygon') + convoy_poly.setAttribute('class', 'convoyorder') + convoy_poly.setAttribute('points', ' '.join(triangle_coord)) + convoy_poly.setAttribute('stroke', self.metadata['color'][power_name]) + + # Inserting + g_node.appendChild(src_shadow_line) + g_node.appendChild(dest_shadow_line) + g_node.appendChild(src_convoy_line) + g_node.appendChild(dest_convoy_line) + g_node.appendChild(shadow_poly) + g_node.appendChild(convoy_poly) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'OrderLayer': + for layer_node in child_node.childNodes: + if layer_node.nodeName == 'g' and _attr(layer_node, 'id') == 'Layer2': + layer_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + def _issue_build_order(self, xml_map, unit_type, loc, power_name): + """ Adds a build army/fleet order to the map + :param xml_map: The xml map being generated + :param unit_type: The unit type to build ('A' or 'F') + :param loc: The province where the army is to be built (e.g. 'PAR') + :param power_name: The name of the power building the unit + :return: Nothing + """ + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -11.5) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], - 10.) + build_loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -20.5) + build_loc_y = _offset(self.metadata['coord'][loc]['unit'][1], -20.5) + + # Symbols + symbol = ARMY if unit_type == 'A' else FLEET + build_symbol = 'BuildUnit' + + # Creating nodes + g_node = xml_map.createElement('g') + + symbol_node = xml_map.createElement('use') + symbol_node.setAttribute('x', loc_x) + symbol_node.setAttribute('y', loc_y) + symbol_node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + symbol_node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + symbol_node.setAttribute('xlink:href', '#{}'.format(symbol)) + symbol_node.setAttribute('class', 'unit{}'.format(power_name.lower())) + + build_node = xml_map.createElement('use') + build_node.setAttribute('x', build_loc_x) + build_node.setAttribute('y', build_loc_y) + build_node.setAttribute('height', self.metadata['symbol_size'][build_symbol][0]) + build_node.setAttribute('width', self.metadata['symbol_size'][build_symbol][1]) + build_node.setAttribute('xlink:href', '#{}'.format(build_symbol)) + + # Inserting + g_node.appendChild(build_node) + g_node.appendChild(symbol_node) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'HighestOrderLayer': + child_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map + + def _issue_disband_order(self, xml_map, loc): + """ Adds a disband order to the map + :param xml_map: The xml map being generated + :param loc: The province where the unit is disbanded (e.g. 'PAR') + :return: Nothing + """ + if self.game.get_current_phase()[-1] == 'R': + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -29.) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], -27.5) + else: + loc_x = _offset(self.metadata['coord'][loc]['unit'][0], -16.5) + loc_y = _offset(self.metadata['coord'][loc]['unit'][1], -15.) + + # Symbols + symbol = 'RemoveUnit' + + # Creating nodes + g_node = xml_map.createElement('g') + symbol_node = xml_map.createElement('use') + symbol_node.setAttribute('x', loc_x) + symbol_node.setAttribute('y', loc_y) + symbol_node.setAttribute('height', self.metadata['symbol_size'][symbol][0]) + symbol_node.setAttribute('width', self.metadata['symbol_size'][symbol][1]) + symbol_node.setAttribute('xlink:href', '#{}'.format(symbol)) + + # Inserting + g_node.appendChild(symbol_node) + for child_node in xml_map.getElementsByTagName('svg')[0].childNodes: + if child_node.nodeName == 'g' and _attr(child_node, 'id') == 'HighestOrderLayer': + child_node.appendChild(g_node) + return xml_map + + # Returning + return xml_map |