mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-19 12:58:09 +00:00
820 lines
35 KiB
Python
820 lines
35 KiB
Python
# ==============================================================================
|
|
# Copyright (C) 2019 - Philip Paquette, Steven Bocco
|
|
#
|
|
# 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/>.
|
|
# ==============================================================================
|
|
""" Helper script to convert a SVG file into a React JS component file.
|
|
Type ``python <script name> --help`` for help.
|
|
"""
|
|
import argparse
|
|
import os
|
|
import re
|
|
from xml.dom import minidom, Node
|
|
|
|
import ujson as json
|
|
|
|
LICENSE_TEXT = """/**
|
|
==============================================================================
|
|
Copyright (C) 2019 - Philip Paquette, Steven Bocco
|
|
|
|
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/>.
|
|
==============================================================================
|
|
**/"""
|
|
|
|
TAG_ORDERDRAWING = 'jdipNS:ORDERDRAWING'
|
|
TAG_POWERCOLORS = 'jdipNS:POWERCOLORS'
|
|
TAG_POWERCOLOR = 'jdipNS:POWERCOLOR'
|
|
TAG_SYMBOLSIZE = 'jdipNS:SYMBOLSIZE'
|
|
TAG_PROVINCE_DATA = 'jdipNS:PROVINCE_DATA'
|
|
TAG_PROVINCE = 'jdipNS:PROVINCE'
|
|
TAG_UNIT = 'jdipNS:UNIT'
|
|
TAG_DISLODGED_UNIT = 'jdipNS:DISLODGED_UNIT'
|
|
TAG_SUPPLY_CENTER = 'jdipNS:SUPPLY_CENTER'
|
|
|
|
SELECTOR_REGEX = re.compile(r'([\r\n][ \t]*)([^{\r\n]+){')
|
|
LINES_REGEX = re.compile(r'[\r\n]+')
|
|
SPACES_REGEX = re.compile(r'[\t ]+')
|
|
STRING_REGEX = re.compile(r'[`\'"] {0,1}\+ {0,1}[`\'"]')
|
|
|
|
|
|
def prepend_css_selectors(prefix, css_text):
|
|
""" Prepend all CSS selector with given prefix (e.g. ID selector) followed by a space.
|
|
|
|
:param prefix: prefix to prepend
|
|
:param css_text: CSS text to parse
|
|
:rtype: str
|
|
"""
|
|
def repl(match):
|
|
return '%s%s %s{' % (match.group(1), prefix, match.group(2))
|
|
|
|
return SELECTOR_REGEX.sub(repl, css_text)
|
|
|
|
|
|
class ExtractedData:
|
|
""" Helper class to store extra data collected while parsing SVG file. Properties:
|
|
|
|
- name: class name of parsed SVG component
|
|
- extra: data parsed from invalid tags found in SVG content
|
|
- style_lines: string lines parsed from <style> tag if found in SVG content
|
|
- id_to_class: dictionary mapping and ID to corresponding class name
|
|
for each tag found with both ID and class name in SVG content.
|
|
"""
|
|
__slots__ = ('name', 'extra', 'style_lines', 'id_to_class')
|
|
|
|
def __init__(self, name):
|
|
""" Initialize extracted data object.
|
|
|
|
:param name: class name of parsed SVG content
|
|
"""
|
|
self.name = name
|
|
self.extra = {}
|
|
self.style_lines = []
|
|
self.id_to_class = {}
|
|
|
|
def get_coordinates(self):
|
|
""" Parse and return unit coordinates from extra field.
|
|
|
|
:return: a dictionary mapping a province name to coordinates [x, y] (as string values)
|
|
for unit ('unit'), dislodged unit ('disl'), and supply center ('sc', if available).
|
|
:rtype: dict
|
|
"""
|
|
coordinates = {}
|
|
for province_definition in self.extra[TAG_PROVINCE_DATA][TAG_PROVINCE]:
|
|
name = province_definition['name'].upper().replace('-', '/')
|
|
coordinates[name] = {}
|
|
if TAG_UNIT in province_definition:
|
|
coordinates[name]['unit'] = [
|
|
province_definition[TAG_UNIT]['x'], province_definition[TAG_UNIT]['y']]
|
|
if TAG_DISLODGED_UNIT in province_definition:
|
|
coordinates[name]['disl'] = [province_definition[TAG_DISLODGED_UNIT]['x'],
|
|
province_definition[TAG_DISLODGED_UNIT]['y']]
|
|
if TAG_SUPPLY_CENTER in province_definition:
|
|
coordinates[name]['sc'] = [province_definition[TAG_SUPPLY_CENTER]['x'],
|
|
province_definition[TAG_SUPPLY_CENTER]['y']]
|
|
return coordinates
|
|
|
|
def get_symbol_sizes(self):
|
|
""" Parse and return symbol sizes from extra field.
|
|
|
|
:return: a dictionary mapping a symbol name to sizes
|
|
('width' and 'height' as floating values).
|
|
:rtype: dict
|
|
"""
|
|
sizes = {}
|
|
for definition in self.extra[TAG_ORDERDRAWING][TAG_SYMBOLSIZE]:
|
|
sizes[definition['name']] = {
|
|
'width': float(definition['width']),
|
|
'height': float(definition['height'])
|
|
}
|
|
return sizes
|
|
|
|
def get_colors(self):
|
|
""" Parse and return power colors from extra field.
|
|
|
|
:return: a dictionary mapping a power name to a HTML color.
|
|
:rtype: dict
|
|
"""
|
|
colors = {}
|
|
for definition in self.extra[TAG_ORDERDRAWING][TAG_POWERCOLORS][TAG_POWERCOLOR]:
|
|
colors[definition['power'].upper()] = definition['color']
|
|
return colors
|
|
|
|
|
|
def safe_react_attribute_name(name):
|
|
""" Convert given raw attribute name into a valid React HTML tag attribute name.
|
|
|
|
:param name: attribute to convert
|
|
:return: valid attribute
|
|
:type name: str
|
|
:rtype: str
|
|
"""
|
|
# Replace 'class' with 'className'
|
|
if name == 'class':
|
|
return 'className'
|
|
# Replace aa-bb-cc with aaBbCc.
|
|
if '-' in name:
|
|
input_pieces = name.split('-')
|
|
output_pieces = [input_pieces[0]]
|
|
for piece in input_pieces[1:]:
|
|
output_pieces.append('%s%s' % (piece[0].upper(), piece[1:]))
|
|
return ''.join(output_pieces)
|
|
if name == 'xlink:href':
|
|
return 'href'
|
|
# Otherwise, return name as-is.
|
|
return name
|
|
|
|
|
|
def compact_extra(extra):
|
|
""" Compact extra dictionary so that it takes less place into final output string.
|
|
|
|
:param extra: dictionary of extra data
|
|
:type extra: dict
|
|
"""
|
|
# pylint:disable=too-many-branches
|
|
if 'children' in extra:
|
|
names = set()
|
|
text_found = False
|
|
for child in extra['children']:
|
|
if isinstance(child, str):
|
|
text_found = True
|
|
else:
|
|
names.add(child['name'])
|
|
if len(names) == len(extra['children']):
|
|
# Each child has a different name, so they cannot be confused,
|
|
# and extra dictionary can be merged with them.
|
|
children_dict = {}
|
|
for child in extra['children']:
|
|
child_name = child.pop('name')
|
|
compact_extra(child)
|
|
children_dict[child_name] = child
|
|
extra.pop('children')
|
|
extra.update(children_dict)
|
|
elif not text_found:
|
|
# Classify children by name.
|
|
classed = {}
|
|
for child in extra['children']:
|
|
classed.setdefault(child['name'], []).append(child)
|
|
# Remove extra['children']
|
|
extra.pop('children')
|
|
for name, children in classed.items():
|
|
if len(children) == 1:
|
|
# This child is the only one with that name. Merge it with extra dictionary.
|
|
child = children[0]
|
|
child.pop('name')
|
|
compact_extra(child)
|
|
extra[name] = child
|
|
else:
|
|
# We found many children with same name.
|
|
# Merge them as a list into extra dictionary.
|
|
values = []
|
|
for child in children:
|
|
child.pop('name')
|
|
compact_extra(child)
|
|
values.append(child)
|
|
extra[name] = values
|
|
else:
|
|
for child in extra['children']:
|
|
compact_extra(child)
|
|
if 'attributes' in extra:
|
|
if not extra['attributes']:
|
|
extra.pop('attributes')
|
|
elif 'name' not in extra or 'name' not in extra['attributes']:
|
|
# Dictionary can be merged with its 'attributes' field.
|
|
extra.update(extra.pop('attributes'))
|
|
|
|
|
|
def extract_extra(node, extra):
|
|
""" Collect extra information from given node into output extra.
|
|
|
|
:type extra: dict
|
|
"""
|
|
extra_dictionary = {'name': node.tagName, 'attributes': {}, 'children': []}
|
|
# Collect attributes.
|
|
for attribute_index in range(node.attributes.length):
|
|
attribute = node.attributes.item(attribute_index)
|
|
extra_dictionary['attributes'][attribute.name] = attribute.value
|
|
# Collect children lines.
|
|
for child in node.childNodes:
|
|
if child.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
|
|
# Child is a text.
|
|
text = child.data.strip()
|
|
if text:
|
|
extra_dictionary['children'].append(text)
|
|
elif child.nodeType != Node.COMMENT_NODE:
|
|
# Child is a normal node. We still consider it as an extra node.
|
|
extract_extra(child, extra_dictionary)
|
|
# Save extra node data into list field extra['children'].
|
|
extra.setdefault('children', []).append(extra_dictionary)
|
|
|
|
|
|
def attributes_to_string(attributes):
|
|
""" Convert given HTML attributes ton an inline string.
|
|
|
|
:param attributes: attributes to write
|
|
:return: a string representing attributes
|
|
:type attributes: dict
|
|
:rtype: str
|
|
"""
|
|
pieces = []
|
|
for name in sorted(attributes):
|
|
value = attributes[name]
|
|
if value.startswith('{'):
|
|
pieces.append('%s=%s' % (name, value))
|
|
else:
|
|
pieces.append('%s="%s"' % (name, value))
|
|
return ' '.join(pieces)
|
|
|
|
|
|
def extract_dom(node, nb_indentation, lines, data):
|
|
""" Parse given node.
|
|
|
|
:param node: (input) node to parse
|
|
:param nb_indentation: (input) number of indentation to use for current node content
|
|
into output lines. 1 indentation is converted to 4 spaces.
|
|
:param lines: (output) collector for output lines of text corresponding to parsed content
|
|
:param data: ExtractedData object to collect extracted data
|
|
(extra, style lines, ID-to-class mapping).
|
|
:type nb_indentation: int
|
|
:type lines: List[str]
|
|
:type data: ExtractedData
|
|
"""
|
|
# pylint: disable=too-many-branches, too-many-statements
|
|
if node.nodeType != Node.ELEMENT_NODE:
|
|
return
|
|
if ':' in node.tagName:
|
|
# Found unhandled tag (example: `<jdipNS:DISPLAY>`).
|
|
# Collect it (and all its descendants) into extra.
|
|
extract_extra(node, data.extra)
|
|
else:
|
|
# Found valid tag.
|
|
attributes = {}
|
|
child_lines = []
|
|
node_id = None
|
|
node_class = None
|
|
# Collect attributes.
|
|
for attribute_index in range(node.attributes.length):
|
|
attribute = node.attributes.item(attribute_index)
|
|
attribute_name = safe_react_attribute_name(attribute.name)
|
|
# Attributes "xmlns:*" are not handled by React. Skip them.
|
|
if not attribute_name.startswith('xmlns:') and attribute_name != 'version':
|
|
attributes[attribute_name] = attribute.value
|
|
if attribute_name == 'id':
|
|
node_id = attribute.value
|
|
elif attribute_name == 'className':
|
|
node_class = attribute.value
|
|
if node_id:
|
|
if node_class:
|
|
# We parameterize class name for this node.
|
|
attributes['className'] = "{classes['%s']}" % node_id
|
|
data.id_to_class[node_id] = node_class
|
|
if node.parentNode.getAttribute('id') == 'MouseLayer':
|
|
# This node must react to onClick and onMouseOver.
|
|
attributes['onClick'] = '{this.onClick}'
|
|
attributes['onMouseOver'] = '{this.onHover}'
|
|
# Collect children lines.
|
|
for child in node.childNodes:
|
|
if child.nodeType in (Node.TEXT_NODE, Node.CDATA_SECTION_NODE):
|
|
# Found a text node.
|
|
text = child.data.strip()
|
|
if text:
|
|
child_lines.append(text)
|
|
else:
|
|
# Found an element node.
|
|
extract_dom(child, nb_indentation + 1, child_lines, data)
|
|
if node.tagName == 'style':
|
|
# Found 'style' tag. Save its children lines into style lines and return immediately,
|
|
data.style_lines.extend(child_lines)
|
|
return
|
|
if node.tagName == 'svg':
|
|
if node_class:
|
|
attributes['className'] += ' %s' % data.name
|
|
else:
|
|
attributes['className'] = data.name
|
|
if node_id:
|
|
if not child_lines:
|
|
if node_id == 'Layer2':
|
|
child_lines.append('{renderedOrders2}')
|
|
elif node_id == 'Layer1':
|
|
child_lines.append('{renderedOrders}')
|
|
elif node_id == 'UnitLayer':
|
|
child_lines.append('{renderedUnits}')
|
|
elif node_id == 'DislodgedUnitLayer':
|
|
child_lines.append('{renderedDislodgedUnits}')
|
|
elif node_id == 'HighestOrderLayer':
|
|
child_lines.append('{renderedHighestOrders}')
|
|
elif node_id == 'CurrentNote':
|
|
child_lines.append("{nb_centers_per_power ? nb_centers_per_power : ''}")
|
|
elif node_id == 'CurrentNote2':
|
|
child_lines.append("{note ? note : ''}")
|
|
if (node_id == 'CurrentPhase'
|
|
and len(child_lines) == 1
|
|
and isinstance(child_lines[0], str)):
|
|
child_lines = ['{current_phase}']
|
|
# We have a normal element node (not style node). Convert it to output lines.
|
|
indentation = ' ' * (4 * nb_indentation)
|
|
attr_string = attributes_to_string(attributes)
|
|
if child_lines:
|
|
# Node must be written as an open tag.
|
|
if len(child_lines) == 1:
|
|
# If we just have 1 child line, write a compact line.
|
|
lines.append(
|
|
'%s<%s%s>%s</%s>' % (
|
|
indentation, node.tagName,
|
|
(' %s' % attr_string) if attr_string else '',
|
|
child_lines[0].lstrip(),
|
|
node.tagName))
|
|
else:
|
|
# Otherwise, write node normally.
|
|
lines.append(
|
|
'%s<%s%s>' % (indentation, node.tagName,
|
|
(' %s' % attr_string) if attr_string else ''))
|
|
lines.extend(child_lines)
|
|
lines.append('%s</%s>' % (indentation, node.tagName))
|
|
else:
|
|
# Node can be written as a close tag.
|
|
lines.append(
|
|
'%s<%s%s/>' % (
|
|
indentation, node.tagName, (' %s' % attr_string) if attr_string else ''))
|
|
|
|
|
|
def to_json_string(dictionary):
|
|
""" Converts to a JSON string, without escaping the '/' characters """
|
|
return json.dumps(dictionary).replace(r'\/', r'/')
|
|
|
|
|
|
def minify(code):
|
|
""" Minifyies a Javascript / CSS file """
|
|
code = LINES_REGEX.sub(' ', code)
|
|
code = SPACES_REGEX.sub(' ', code)
|
|
code = STRING_REGEX.sub(' ', code)
|
|
return code
|
|
|
|
|
|
def main():
|
|
""" Main script function. """
|
|
parser = argparse.ArgumentParser(
|
|
prog='Convert a SVG file to a React Component.'
|
|
)
|
|
parser.add_argument('--input', '-i', type=str, required=True, help='SVG file to convert.')
|
|
parser.add_argument('--name', '-n', type=str, required=True, help="Component name.")
|
|
parser.add_argument('--output', '-o', type=str, default=os.getcwd(),
|
|
help='Output folder (default to working folder).')
|
|
args = parser.parse_args()
|
|
root = minidom.parse(args.input).documentElement
|
|
class_name = args.name
|
|
output_folder = args.output
|
|
if not os.path.exists(output_folder):
|
|
os.makedirs(output_folder)
|
|
assert os.path.isdir(output_folder), 'Not a directory: %s' % output_folder
|
|
extra_class_name = '%sMetadata' % class_name
|
|
lines = []
|
|
data = ExtractedData(class_name)
|
|
extract_dom(root, 3, lines, data)
|
|
compact_extra(data.extra)
|
|
|
|
output_file_name = os.path.join(output_folder, '%s.js' % class_name)
|
|
style_file_name = os.path.join(output_folder, '%s.css' % class_name)
|
|
extra_parsed_file_name = os.path.join(output_folder, '%s.js' % extra_class_name)
|
|
|
|
# CSS
|
|
if data.style_lines:
|
|
with open(style_file_name, 'w') as style_file:
|
|
style_file.write(LICENSE_TEXT)
|
|
style_file.write('\n')
|
|
style_file.writelines(
|
|
minify(prepend_css_selectors('.%s' % class_name, '\n'.join(data.style_lines))))
|
|
|
|
# Metadata
|
|
if data.extra:
|
|
with open(extra_parsed_file_name, 'w') as extra_parsed_file:
|
|
extra_parsed_file.write("""%(license_text)s
|
|
export const Coordinates = %(coordinates)s;
|
|
export const SymbolSizes = %(symbol_sizes)s;
|
|
export const Colors = %(colors)s;
|
|
""" % {'license_text': LICENSE_TEXT,
|
|
'coordinates': to_json_string(data.get_coordinates()),
|
|
'symbol_sizes': to_json_string(data.get_symbol_sizes()),
|
|
'colors': to_json_string(data.get_colors())})
|
|
|
|
# Map javacript
|
|
map_js_code = ("""
|
|
import React from 'react';
|
|
import PropTypes from 'prop-types';
|
|
%(style_content)s
|
|
%(extra_content)s
|
|
import {getClickedID, parseLocation, setInfluence} from "../common/common";
|
|
import {Game} from "../../../diplomacy/engine/game";
|
|
import {MapData} from "../../utils/map_data";
|
|
import {UTILS} from "../../../diplomacy/utils/utils";
|
|
import {Diplog} from "../../../diplomacy/utils/diplog";
|
|
import {extendOrderBuilding} from "../../utils/order_building";
|
|
import {Unit} from "../common/unit";
|
|
import {Hold} from "../common/hold";
|
|
import {Move} from "../common/move";
|
|
import {SupportMove} from "../common/supportMove";
|
|
import {SupportHold} from "../common/supportHold";
|
|
import {Convoy} from "../common/convoy";
|
|
import {Build} from "../common/build";
|
|
import {Disband} from "../common/disband";
|
|
|
|
export class %(classname)s extends React.Component {
|
|
constructor(props) {
|
|
super(props);
|
|
this.onClick = this.onClick.bind(this);
|
|
this.onHover = this.onHover.bind(this);
|
|
}
|
|
onClick(event) {
|
|
if (this.props.orderBuilding)
|
|
return this.handleClickedID(getClickedID(event));
|
|
}
|
|
onHover(event) {
|
|
return this.handleHoverID(getClickedID(event));
|
|
}
|
|
handleClickedID(id) {
|
|
const orderBuilding = this.props.orderBuilding;
|
|
if (!orderBuilding.builder)
|
|
return this.props.onError('No orderable locations.');
|
|
const province = this.props.mapData.getProvince(id);
|
|
if (!province)
|
|
throw new Error(`Cannot find a province named ${id}`);
|
|
|
|
const stepLength = orderBuilding.builder.steps.length;
|
|
if (orderBuilding.path.length >= stepLength)
|
|
throw new Error(`Order building: current steps count (${orderBuilding.path.length}) should be less than` +
|
|
` expected steps count (${stepLength}) (${orderBuilding.path.join(', ')}).`);
|
|
|
|
const lengthAfterClick = orderBuilding.path.length + 1;
|
|
let validLocations = [];
|
|
const testedPath = [orderBuilding.type].concat(orderBuilding.path);
|
|
const value = UTILS.javascript.getTreeValue(this.props.game.ordersTree, testedPath);
|
|
if (value !== null) {
|
|
const checker = orderBuilding.builder.steps[lengthAfterClick - 1];
|
|
try {
|
|
const possibleLocations = checker(province, orderBuilding.power);
|
|
for (let possibleLocation of possibleLocations) {
|
|
possibleLocation = possibleLocation.toUpperCase();
|
|
if (value.includes(possibleLocation))
|
|
validLocations.push(possibleLocation);
|
|
}
|
|
} catch (error) {
|
|
return this.props.onError(error);
|
|
}
|
|
}
|
|
if (!validLocations.length)
|
|
return this.props.onError('Disallowed.');
|
|
|
|
if (validLocations.length > 1 && orderBuilding.type === 'S' && orderBuilding.path.length >= 2) {
|
|
/* We are building a support order and we have a multiple choice for a location. */
|
|
/* Let's check if next location to choose is a coast. To have a coast: */
|
|
/* - all possible locations must start with same 3 characters. */
|
|
/* - we expect at least province name in possible locations (e.g. 'SPA' for 'SPA/NC'). */
|
|
/* If we have a coast, we will remove province name from possible locations. */
|
|
let isACoast = true;
|
|
let validLocationsNoProvinceName = [];
|
|
for (let i = 0; i < validLocations.length; ++i) {
|
|
let location = validLocations[i];
|
|
if (i > 0) {
|
|
/* Compare 3 first letters with previous location. */
|
|
if (validLocations[i - 1].substring(0, 3).toUpperCase() !== validLocations[i].substring(0, 3).toUpperCase()) {
|
|
/* No same prefix with previous location. We does not have a coast. */
|
|
isACoast = false;
|
|
break;
|
|
}
|
|
}
|
|
if (location.length !== 3)
|
|
validLocationsNoProvinceName.push(location);
|
|
}
|
|
if (validLocations.length === validLocationsNoProvinceName.length) {
|
|
/* We have not found province name. */
|
|
isACoast = false;
|
|
}
|
|
if (isACoast) {
|
|
/* We want to choose location in a coastal province. Let's remove province name. */
|
|
validLocations = validLocationsNoProvinceName;
|
|
}
|
|
}
|
|
|
|
if (validLocations.length > 1) {
|
|
if (this.props.onSelectLocation) {
|
|
return this.props.onSelectLocation(validLocations, orderBuilding.power, orderBuilding.type, orderBuilding.path);
|
|
} else {
|
|
Diplog.warn(`Forced to select first valid location.`);
|
|
validLocations = [validLocations[0]];
|
|
}
|
|
}
|
|
let orderBuildingType = orderBuilding.type;
|
|
if (lengthAfterClick === stepLength && orderBuildingType === 'M') {
|
|
const moveOrderPath = ['M'].concat(orderBuilding.path, validLocations[0]);
|
|
const moveTypes = UTILS.javascript.getTreeValue(this.props.game.ordersTree, moveOrderPath);
|
|
if (moveTypes !== null) {
|
|
if (moveTypes.length === 2 && this.props.onSelectVia) {
|
|
/* This move can be done either regularly or VIA a fleet. Let user choose. */
|
|
return this.props.onSelectVia(validLocations[0], orderBuilding.power, orderBuilding.path);
|
|
} else {
|
|
orderBuildingType = moveTypes[0];
|
|
}
|
|
}
|
|
}
|
|
extendOrderBuilding(
|
|
orderBuilding.power, orderBuildingType, orderBuilding.path, validLocations[0],
|
|
this.props.onOrderBuilding, this.props.onOrderBuilt, this.props.onError
|
|
);
|
|
}
|
|
handleHoverID(id) {
|
|
if (this.props.onHover) {
|
|
const province = this.props.mapData.getProvince(id);
|
|
if (province) {
|
|
this.props.onHover(province.name, this.getRelatedOrders(province.name));
|
|
}
|
|
}
|
|
}
|
|
getRelatedOrders(name) {
|
|
const orders = [];
|
|
if (this.props.orders) {
|
|
for (let powerOrders of Object.values(this.props.orders)) {
|
|
if (powerOrders) {
|
|
for (let order of powerOrders) {
|
|
const pieces = order.split(/ +/);
|
|
if (pieces[1].slice(0, 3) === name.toUpperCase().slice(0, 3))
|
|
orders.push(order);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return orders;
|
|
}
|
|
getNeighbors(extraLocation) {
|
|
const selectedPath = [this.props.orderBuilding.type].concat(this.props.orderBuilding.path);
|
|
if (extraLocation)
|
|
selectedPath.push(extraLocation);
|
|
const possibleNeighbors = UTILS.javascript.getTreeValue(this.props.game.ordersTree, selectedPath);
|
|
const neighbors = possibleNeighbors ? possibleNeighbors.map(neighbor => parseLocation(neighbor)) : [];
|
|
return neighbors.length ? neighbors: null;
|
|
}
|
|
render() {
|
|
const classes = %(classes)s;
|
|
const game = this.props.game;
|
|
const mapData = this.props.mapData;
|
|
const orders = this.props.orders;
|
|
|
|
/* Current phase. */
|
|
const current_phase = (game.phase[0] === '?' || game.phase === 'COMPLETED') ? 'FINAL' : game.phase;
|
|
|
|
/* Notes. */
|
|
const nb_centers = [];
|
|
for (let power of Object.values(game.powers)) {
|
|
if (!power.isEliminated())
|
|
nb_centers.push([power.name.substr(0, 3), power.centers.length]);
|
|
}
|
|
/* Sort nb_centers by descending number of centers. */
|
|
nb_centers.sort((a, b) => {
|
|
return -(a[1] - b[1]) || a[0].localeCompare(b[0]);
|
|
});
|
|
const nb_centers_per_power = nb_centers.map((couple) => (couple[0] + ': ' + couple[1])).join(' ');
|
|
const note = game.note;
|
|
|
|
/* Adding units, influence and orders. */
|
|
const renderedUnits = [];
|
|
const renderedDislodgedUnits = [];
|
|
const renderedOrders = [];
|
|
const renderedOrders2 = [];
|
|
const renderedHighestOrders = [];
|
|
for (let power of Object.values(game.powers)) if (!power.isEliminated()) {
|
|
for (let unit of power.units) {
|
|
renderedUnits.push(
|
|
<Unit key={unit}
|
|
unit={unit}
|
|
powerName={power.name}
|
|
isDislodged={false}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}/>
|
|
);
|
|
}
|
|
for (let unit of Object.keys(power.retreats)) {
|
|
renderedDislodgedUnits.push(
|
|
<Unit key={unit}
|
|
unit={unit}
|
|
powerName={power.name}
|
|
isDislodged={true}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}/>
|
|
);
|
|
}
|
|
for (let center of power.centers) {
|
|
setInfluence(classes, mapData, center, power.name);
|
|
}
|
|
for (let loc of power.influence) {
|
|
if (!mapData.supplyCenters.has(loc))
|
|
setInfluence(classes, mapData, loc, power.name);
|
|
}
|
|
|
|
if (orders) {
|
|
const powerOrders = (orders && orders.hasOwnProperty(power.name) && orders[power.name]) || [];
|
|
for (let order of powerOrders) {
|
|
const tokens = order.split(/ +/);
|
|
if (!tokens || tokens.length < 3)
|
|
continue;
|
|
const unit_loc = tokens[1];
|
|
if (tokens[2] === 'H') {
|
|
renderedOrders.push(
|
|
<Hold key={order}
|
|
loc={unit_loc}
|
|
powerName={power.name}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}
|
|
colors={Colors}/>
|
|
);
|
|
} else if (tokens[2] === '-') {
|
|
const destLoc = tokens[tokens.length - (tokens[tokens.length - 1] === 'VIA' ? 2 : 1)];
|
|
renderedOrders.push(
|
|
<Move key={order}
|
|
srcLoc={unit_loc}
|
|
dstLoc={destLoc}
|
|
powerName={power.name}
|
|
phaseType={game.getPhaseType()}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}
|
|
colors={Colors}/>
|
|
);
|
|
} else if (tokens[2] === 'S') {
|
|
const destLoc = tokens[tokens.length - 1];
|
|
if (tokens.includes('-')) {
|
|
const srcLoc = tokens[4];
|
|
renderedOrders2.push(
|
|
<SupportMove key={order}
|
|
loc={unit_loc}
|
|
srcLoc={srcLoc}
|
|
dstLoc={destLoc}
|
|
powerName={power.name}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}
|
|
colors={Colors}/>
|
|
);
|
|
} else {
|
|
renderedOrders2.push(
|
|
<SupportHold key={order}
|
|
loc={unit_loc}
|
|
dstLoc={destLoc}
|
|
powerName={power.name}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}
|
|
colors={Colors}/>
|
|
);
|
|
}
|
|
} else if (tokens[2] === 'C') {
|
|
const srcLoc = tokens[4];
|
|
const destLoc = tokens[tokens.length - 1];
|
|
if ((srcLoc !== destLoc) && (tokens.includes('-'))) {
|
|
renderedOrders2.push(
|
|
<Convoy key={order}
|
|
loc={unit_loc}
|
|
srcLoc={srcLoc}
|
|
dstLoc={destLoc}
|
|
powerName={power.name}
|
|
coordinates={Coordinates} colors={Colors}
|
|
symbolSizes={SymbolSizes}/>
|
|
);
|
|
}
|
|
} else if (tokens[2] === 'B') {
|
|
renderedHighestOrders.push(
|
|
<Build key={order}
|
|
unitType={tokens[0]}
|
|
loc={unit_loc}
|
|
powerName={power.name}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}/>
|
|
);
|
|
} else if (tokens[2] === 'D') {
|
|
renderedHighestOrders.push(
|
|
<Disband key={order}
|
|
loc={unit_loc}
|
|
phaseType={game.getPhaseType()}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}/>
|
|
);
|
|
} else if (tokens[2] === 'R') {
|
|
const destLoc = tokens[3];
|
|
renderedOrders.push(
|
|
<Move key={order}
|
|
srcLoc={unit_loc}
|
|
dstLoc={destLoc}
|
|
powerName={power.name}
|
|
phaseType={game.getPhaseType()}
|
|
coordinates={Coordinates}
|
|
symbolSizes={SymbolSizes}
|
|
colors={Colors}/>
|
|
);
|
|
} else {
|
|
throw new Error(`Unknown error to render (${order}).`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.props.orderBuilding && this.props.orderBuilding.path.length) {
|
|
const clicked = parseLocation(this.props.orderBuilding.path[0]);
|
|
const province = this.props.mapData.getProvince(clicked);
|
|
if (!province)
|
|
throw new Error(('Unknown clicked province ' + clicked));
|
|
const clickedID = province.getID(classes);
|
|
if (!clicked)
|
|
throw new Error(`Unknown path (${clickedID}) for province (${clicked}).`);
|
|
classes[clickedID] = 'provinceRed';
|
|
const neighbors = this.getNeighbors();
|
|
if (neighbors) {
|
|
for (let neighbor of neighbors) {
|
|
const neighborProvince = this.props.mapData.getProvince(neighbor);
|
|
if (!neighborProvince)
|
|
throw new Error('Unknown neighbor province ' + neighbor);
|
|
const neighborID = neighborProvince.getID(classes);
|
|
if (!neighborID)
|
|
throw new Error(`Unknown neoghbor path (${neighborID}) for province (${neighbor}).`);
|
|
classes[neighborID] = neighborProvince.isWater() ? 'provinceBlue' : 'provinceGreen';
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.props.showAbbreviations === false) {
|
|
classes['BriefLabelLayer'] = 'visibilityHidden';
|
|
}
|
|
|
|
return (
|
|
%(svg)s
|
|
);
|
|
}
|
|
}
|
|
%(classname)s.propTypes = {
|
|
game: PropTypes.instanceOf(Game).isRequired,
|
|
mapData: PropTypes.instanceOf(MapData).isRequired,
|
|
orders: PropTypes.object,
|
|
onHover: PropTypes.func,
|
|
onError: PropTypes.func.isRequired,
|
|
onSelectLocation: PropTypes.func,
|
|
onSelectVia: PropTypes.func,
|
|
onOrderBuilding: PropTypes.func,
|
|
onOrderBuilt: PropTypes.func,
|
|
orderBuilding: PropTypes.object,
|
|
showAbbreviations: PropTypes.bool
|
|
};
|
|
""" % {'style_content': "import './%s.css';" % class_name if data.style_lines else '',
|
|
'extra_content': 'import {Coordinates, SymbolSizes, Colors} from "./%s";' %
|
|
(extra_class_name) if data.extra else '',
|
|
'classname': class_name,
|
|
'classes': to_json_string(data.id_to_class),
|
|
'svg': '\n'.join(lines)})
|
|
|
|
# Adding license and minifying
|
|
map_js_code = LICENSE_TEXT \
|
|
+ '\n/** Generated with parameters: %s **/\n' % args \
|
|
+ minify(map_js_code) \
|
|
+ '// eslint-disable-line semi'
|
|
|
|
# Writing to disk
|
|
with open(output_file_name, 'w') as file:
|
|
file.write(map_js_code)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|