mirror of
https://github.com/GoodStartLabs/AI_Diplomacy.git
synced 2026-04-25 17:10:53 +00:00
676 lines
31 KiB
Python
676 lines
31 KiB
Python
# ==============================================================================
|
||
# Copyright 2019 - Philip Paquette. All Rights Reserved.
|
||
#
|
||
# NOTICE: All information contained herein is, and remains the property of the authors
|
||
# listed above. The intellectual and technical concepts contained herein are proprietary
|
||
# and may be covered by U.S. and Foreign Patents, patents in process, and are protected
|
||
# by trade secret or copyright law. Dissemination of this information or reproduction of
|
||
# this material is strictly forbidden unless prior written permission is obtained.
|
||
# ==============================================================================
|
||
""" Orders - Contains utilities to convert orders between string format and webdiplomacy.net format """
|
||
import logging
|
||
from queue import Queue
|
||
from diplomacy import Map
|
||
from diplomacy.integration.webdiplomacy_net.utils import CACHE
|
||
|
||
# Constants
|
||
LOGGER = logging.getLogger(__name__)
|
||
|
||
def is_adjacent_for_convoy(loc_1, loc_2, map_object):
|
||
""" Checks if two locations are adjacent (for convoy purposes)
|
||
|
||
- If loc_1 and loc_2 are water, loc_1 and loc_2 must be adjacent
|
||
- If loc_1 or loc_2 is land, then they are not adjacent
|
||
- If loc_1 or loc_2 are coast, then the other locations needs to be a water loc at most 1 loc away
|
||
|
||
:type map_object: diplomacy.Map
|
||
"""
|
||
area_1 = map_object.area_type(loc_1)
|
||
area_2 = map_object.area_type(loc_2)
|
||
|
||
# Lands can't be used for convoys, so they are never adjacent
|
||
if area_1 == 'LAND' or area_2 == 'LAND':
|
||
return False
|
||
|
||
# If both units are on water, checking if they are adjacent
|
||
if area_1 == 'WATER' and area_2 == 'WATER':
|
||
return map_object.abuts('F', loc_1, '-', loc_2)
|
||
|
||
# Moving from coast to water or vice-versa
|
||
if area_2 == 'COAST' and area_1 == 'WATER':
|
||
return is_adjacent_for_convoy(loc_2, loc_1, map_object)
|
||
if area_1 == 'COAST' and area_2 == 'WATER':
|
||
for loc_with_coast in map_object.find_coasts(loc_1):
|
||
if map_object.abuts('F', loc_with_coast, '-', loc_2):
|
||
return True
|
||
|
||
# Otherwise, not adjacent
|
||
return False
|
||
|
||
def find_convoy_path(src, dest, map_object, game=None, including=None, excluding=None):
|
||
""" Finds a convoy path from src to dest
|
||
|
||
:param src: The source location (e.g. 'BRE')
|
||
:param dest: The destination location (e.g. 'LON')
|
||
:param map_object: A diplomacy.Map object representation of the current map
|
||
:param game: Optional. The current game object to retrieve the list of fleets.
|
||
:param including: Optional. A single province (e.g. 'NAO') or a list of provinces that must be in the path.
|
||
:param excluding: Optional. A single province (e.g. 'NAO') or a list of provinces that must NOT be in the path.
|
||
:return: Either an empty list if a convoy is not possible between src and dest
|
||
or a list of [src, fleet1, fleet2, ..., fleet_n, dest] to use to convoy A `src` - `dest`.
|
||
:type map_object: diplomacy.Map
|
||
:type game: diplomacy.Game
|
||
"""
|
||
if map_object.area_type(src) != 'COAST' or map_object.area_type(dest) != 'COAST':
|
||
return []
|
||
|
||
# Converting including and excluding to a list
|
||
if not isinstance(including, list):
|
||
including = [including] if including is not None else []
|
||
if not isinstance(excluding, list):
|
||
excluding = [excluding] if excluding is not None else []
|
||
|
||
# Finding all water locs
|
||
water_locs = {loc.upper() for loc in map_object.locs if map_object.area_type(loc.upper()) == 'WATER'}
|
||
|
||
# Finding all convoyers
|
||
convoyers = water_locs
|
||
if game is not None:
|
||
convoyers = set()
|
||
for power_units in game.get_units().values():
|
||
convoyers |= {unit[2:] for unit in power_units if unit[0] == 'F' and unit[2:] in water_locs}
|
||
|
||
# Finding the minimum set of units that would allow a convoy and that matches all the conditions
|
||
fleets_in_convoy = set()
|
||
for nb_fleets in map_object.convoy_paths:
|
||
for start_loc, fleet_locs, dest_locs in map_object.convoy_paths[nb_fleets]:
|
||
if start_loc != src or dest not in dest_locs: # Src or dest do not match
|
||
continue
|
||
if not fleet_locs.issubset(convoyers): # Missing some convoyers to use this path
|
||
continue
|
||
if fleet_locs.intersection(including) != set(including): # Missing some fleets needed to be incl.
|
||
continue
|
||
if fleet_locs.intersection(excluding): # Contains fleets needed to be excl.
|
||
continue
|
||
fleets_in_convoy = fleet_locs
|
||
break
|
||
if fleets_in_convoy:
|
||
break
|
||
else:
|
||
return [] # No convoy path found.
|
||
|
||
# Finding a path from src to dest using those convoys
|
||
# Using breadth first search
|
||
queue = Queue()
|
||
item = (src,), fleets_in_convoy
|
||
queue.put(item)
|
||
|
||
while not queue.empty():
|
||
current_path, remaining_fleets = queue.get()
|
||
|
||
# Checking if this path is valid
|
||
if len(current_path) > 1 and is_adjacent_for_convoy(current_path[-1], dest, map_object):
|
||
return list(current_path) + [dest]
|
||
|
||
# Trying to add all remaining fleets
|
||
for fleet in remaining_fleets:
|
||
if not is_adjacent_for_convoy(current_path[-1], fleet, map_object):
|
||
continue
|
||
new_path = tuple(list(current_path) + [fleet])
|
||
item = new_path, remaining_fleets - {fleet}
|
||
queue.put(item)
|
||
|
||
# No paths found
|
||
return []
|
||
|
||
|
||
class Order:
|
||
""" Class to convert order from string representation to dictionary (webdiplomacy.net) representation """
|
||
|
||
def __init__(self, order, map_id=None, map_name=None, phase_type=None, game=None):
|
||
""" Constructor
|
||
|
||
:param order: An order (either as a string 'A PAR H' or as a dictionary)
|
||
:param map_id: Optional. The map id of the map where orders are submitted (webdiplomacy format)
|
||
:param map_name: Optional. The map name of the map where orders are submitted.
|
||
:param phase_type: Optional. The phase type ('M', 'R', 'A') to disambiguate orders to send.
|
||
:param game: Optional. The diplomacy.Game object to build the correct convoy path.
|
||
"""
|
||
self.map_name = 'standard'
|
||
self.phase_type = 'M'
|
||
self.order_str = ''
|
||
self.order_dict = {}
|
||
|
||
# Detecting the map name
|
||
if map_id is not None:
|
||
if map_id not in CACHE['ix_to_map']:
|
||
raise ValueError('Map with id %s is not supported.' % map_id)
|
||
self.map_name = CACHE['ix_to_map'][map_id]
|
||
elif map_name is not None:
|
||
if map_name not in CACHE['map_to_ix']:
|
||
raise ValueError('Map with name %s is not supported.' % map_name)
|
||
self.map_name = map_name
|
||
|
||
# Detecting the phase type
|
||
if isinstance(phase_type, str) and phase_type in 'MRA':
|
||
self.phase_type = phase_type
|
||
|
||
# Building the order
|
||
if isinstance(order, str):
|
||
self._build_from_string(order, game=game)
|
||
elif isinstance(order, dict):
|
||
self._build_from_dict(order)
|
||
else:
|
||
raise ValueError('Expected order to be a string or a dictionary.')
|
||
|
||
def _build_from_string(self, order, game=None):
|
||
""" Builds this object from a string
|
||
|
||
:type order: str
|
||
:type game: diplomacy.Game
|
||
"""
|
||
# pylint: disable=too-many-return-statements,too-many-branches,too-many-statements
|
||
# Converting move to retreat during retreat phase
|
||
if self.phase_type == 'R':
|
||
order = order.replace(' - ', ' R ')
|
||
|
||
# Splitting into parts
|
||
words = order.split()
|
||
|
||
# --- Wait / Waive ---
|
||
# [{"id": "56", "unitID": null, "type": "Wait", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}]
|
||
if len(words) == 1 and words[0] == 'WAIVE':
|
||
self.order_str = 'WAIVE'
|
||
self.order_dict = {'terrID': None,
|
||
'unitType': '',
|
||
'type': 'Wait',
|
||
'toTerrID': '',
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
return
|
||
|
||
# Validating
|
||
if len(words) < 3:
|
||
LOGGER.error('Unable to parse the order "%s". Require at least 3 words', order)
|
||
return
|
||
|
||
short_unit_type, loc_name, order_type = words[:3]
|
||
if short_unit_type not in 'AF':
|
||
LOGGER.error('Unable to parse the order "%s". Valid unit types are "A" and "F".', order)
|
||
return
|
||
if order_type not in 'H-SCRBD':
|
||
LOGGER.error('Unable to parse the order "%s". Valid order types are H-SCRBD', order)
|
||
return
|
||
if loc_name not in CACHE[self.map_name]['loc_to_ix']:
|
||
LOGGER.error('Received invalid loc "%s" for map "%s".', loc_name, self.map_name)
|
||
return
|
||
|
||
# Extracting territories
|
||
unit_type = {'A': 'Army', 'F': 'Fleet'}[short_unit_type]
|
||
terr_id = CACHE[self.map_name]['loc_to_ix'][loc_name]
|
||
|
||
# --- Hold ---
|
||
# {"id": "76", "unitID": "19", "type": "Hold", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
|
||
if order_type == 'H':
|
||
self.order_str = '%s %s H' % (short_unit_type, loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Hold',
|
||
'toTerrID': '',
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Move ---
|
||
# {"id": "73", "unitID": "16", "type": "Move", "toTerrID": "25", "fromTerrID": "", "viaConvoy": "Yes",
|
||
# "convoyPath": ["22", "69"]},
|
||
# {"id": "74", "unitID": "17", "type": "Move", "toTerrID": "69", "fromTerrID": "", "viaConvoy": "No"}
|
||
elif order_type == '-':
|
||
if len(words) < 4:
|
||
LOGGER.error('[Move] Unable to parse the move order "%s". Require at least 4 words', order)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# Building map
|
||
map_object = Map(self.map_name)
|
||
convoy_path = []
|
||
|
||
# Getting destination
|
||
to_loc_name = words[3]
|
||
to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)
|
||
|
||
# Deciding if this move is doable by convoy or not
|
||
if unit_type != 'Army':
|
||
via_flag = ''
|
||
else:
|
||
# Any plausible convoy path (i.e. where fleets are on water, even though they are not convoying)
|
||
# is valid for the 'convoyPath' argument
|
||
reachable_by_land = map_object.abuts('A', loc_name, '-', to_loc_name)
|
||
via_convoy = bool(words[-1] == 'VIA') or not reachable_by_land
|
||
via_flag = ' VIA' if via_convoy else ''
|
||
convoy_path = find_convoy_path(loc_name, to_loc_name, map_object, game)
|
||
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Move] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
self.order_str = '%s %s - %s%s' % (short_unit_type, loc_name, to_loc_name, via_flag)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Move',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': 'Yes' if via_flag else 'No'}
|
||
if convoy_path:
|
||
self.order_dict['convoyPath'] = [CACHE[self.map_name]['loc_to_ix'][loc] for loc in convoy_path[:-1]]
|
||
|
||
# --- Support hold ---
|
||
# {"id": "73", "unitID": "16", "type": "Support hold", "toTerrID": "24", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'S' and '-' not in words:
|
||
if len(words) < 5:
|
||
LOGGER.error('[Support H] Unable to parse the support hold order "%s". Require at least 5 words', order)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# Getting supported unit
|
||
to_loc_name = words[4][:3]
|
||
to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)
|
||
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Support H] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
self.order_str = '%s %s S %s' % (short_unit_type, loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Support hold',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Support move ---
|
||
# {"id": "73", "unitID": "16", "type": "Support move", "toTerrID": "24", "fromTerrID": "69", "viaConvoy": ""}
|
||
elif order_type == 'S':
|
||
if len(words) < 6:
|
||
LOGGER.error('Unable to parse the support move order "%s". Require at least 6 words', order)
|
||
return
|
||
|
||
# Building map
|
||
map_object = Map(self.map_name)
|
||
convoy_path = []
|
||
|
||
# Getting supported unit
|
||
move_index = words.index('-')
|
||
to_loc_name = words[move_index + 1][:3] # Removing coast from dest
|
||
from_loc_name = words[move_index - 1]
|
||
to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)
|
||
from_terr_id = CACHE[self.map_name]['loc_to_ix'].get(from_loc_name, None)
|
||
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Support M] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
if from_loc_name is None:
|
||
LOGGER.error('[Support M] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# Deciding if we are support a move by convoy or not
|
||
# Any plausible convoy path (i.e. where fleets are on water, even though they are not convoying)
|
||
# is valid for the 'convoyPath' argument, only if it does not include the fleet issuing the support
|
||
if words[move_index - 2] != 'F' and map_object.area_type(from_loc_name) == 'COAST':
|
||
convoy_path = find_convoy_path(from_loc_name, to_loc_name, map_object, game, excluding=loc_name)
|
||
|
||
self.order_str = '%s %s S %s - %s' % (short_unit_type, loc_name, from_loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Support move',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': from_terr_id,
|
||
'viaConvoy': ''}
|
||
if convoy_path:
|
||
self.order_dict['convoyPath'] = [CACHE[self.map_name]['loc_to_ix'][loc] for loc in convoy_path[:-1]]
|
||
|
||
# --- Convoy ---
|
||
# {"id": "79", "unitID": "22", "type": "Convoy", "toTerrID": "24", "fromTerrID": "20", "viaConvoy": "",
|
||
# "convoyPath": ["20", "69"]}
|
||
elif order_type == 'C':
|
||
if len(words) < 6:
|
||
LOGGER.error('[Convoy] Unable to parse the convoy order "%s". Require at least 6 words', order)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# Building map
|
||
map_object = Map(self.map_name)
|
||
|
||
# Getting supported unit
|
||
move_index = words.index('-')
|
||
to_loc_name = words[move_index + 1]
|
||
from_loc_name = words[move_index - 1]
|
||
to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)
|
||
from_terr_id = CACHE[self.map_name]['loc_to_ix'].get(from_loc_name, None)
|
||
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Convoy] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
if from_loc_name is None:
|
||
LOGGER.error('[Convoy] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# Finding convoy path
|
||
# Any plausible convoy path (i.e. where fleets are on water, even though they are not convoying)
|
||
# is valid for the 'convoyPath' argument, only if it includes the current fleet issuing the convoy order
|
||
convoy_path = find_convoy_path(from_loc_name, to_loc_name, map_object, game, including=loc_name)
|
||
|
||
self.order_str = '%s %s C A %s - %s' % (short_unit_type, loc_name, from_loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Convoy',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': from_terr_id,
|
||
'viaConvoy': ''}
|
||
if convoy_path:
|
||
self.order_dict['convoyPath'] = [CACHE[self.map_name]['loc_to_ix'][loc] for loc in convoy_path[:-1]]
|
||
|
||
# --- Retreat ---
|
||
# {"id": "152", "unitID": "18", "type": "Retreat", "toTerrID": "75", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'R':
|
||
if len(words) < 4:
|
||
LOGGER.error('[Retreat] Unable to parse the move order "%s". Require at least 4 words', order)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# Getting destination
|
||
to_loc_name = words[3]
|
||
to_terr_id = CACHE[self.map_name]['loc_to_ix'].get(to_loc_name, None)
|
||
|
||
if to_loc_name is None:
|
||
return
|
||
|
||
self.order_str = '%s %s R %s' % (short_unit_type, loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Retreat',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Disband (R phase) ---
|
||
# {"id": "152", "unitID": "18", "type": "Disband", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'D' and self.phase_type == 'R':
|
||
# Note: For R phase, we disband with the coast
|
||
self.order_str = '%s %s D' % (short_unit_type, loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Disband',
|
||
'toTerrID': '',
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Build Army ---
|
||
# [{"id": "56", "unitID": null, "type": "Build Army", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
|
||
elif order_type == 'B' and short_unit_type == 'A':
|
||
self.order_str = 'A %s B' % loc_name
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': 'Army',
|
||
'type': 'Build Army',
|
||
'toTerrID': terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# -- Build Fleet ---
|
||
# [{"id": "56", "unitID": null, "type": "Build Fleet", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
|
||
elif order_type == 'B' and short_unit_type == 'F':
|
||
self.order_str = 'F %s B' % loc_name
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': 'Fleet',
|
||
'type': 'Build Fleet',
|
||
'toTerrID': terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# Disband (A phase)
|
||
# {"id": "152", "unitID": null, "type": "Destroy", "toTerrID": "18", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'D':
|
||
# For A phase, we disband without the coast
|
||
loc_name = loc_name[:3]
|
||
terr_id = CACHE[self.map_name]['loc_to_ix'][loc_name]
|
||
self.order_str = '%s %s D' % (short_unit_type, loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Destroy',
|
||
'toTerrID': terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
def _build_from_dict(self, order):
|
||
""" Builds this object from a dictionary
|
||
|
||
:type order: dict
|
||
"""
|
||
# pylint: disable=too-many-return-statements
|
||
terr_id = order.get('terrID', None)
|
||
unit_type = order.get('unitType', None)
|
||
order_type = order.get('type', None)
|
||
to_terr_id = order.get('toTerrID', '')
|
||
from_terr_id = order.get('fromTerrID', '')
|
||
via_convoy = order.get('viaConvoy', '')
|
||
|
||
# Using to_terr_id if terr_id is None
|
||
terr_id = terr_id if terr_id is not None else to_terr_id
|
||
|
||
# Overriding unit type for builds
|
||
if order_type == 'Build Army':
|
||
unit_type = 'Army'
|
||
elif order_type == 'Build Fleet':
|
||
unit_type = 'Fleet'
|
||
elif order_type in ('Destroy', 'Wait') and unit_type not in ('Army', 'Fleet'):
|
||
unit_type = '?'
|
||
|
||
# Validating order
|
||
if unit_type not in ('Army', 'Fleet', '?'):
|
||
LOGGER.error('Received invalid unit type "%s". Expected "Army" or "Fleet".', unit_type)
|
||
return
|
||
if order_type not in ('Hold', 'Move', 'Support hold', 'Support move', 'Convoy', 'Retreat', 'Disband',
|
||
'Build Army', 'Build Fleet', 'Wait', 'Destroy'):
|
||
LOGGER.error('Received invalid order type "%s".', order_type)
|
||
return
|
||
if terr_id not in CACHE[self.map_name]['ix_to_loc'] and terr_id is not None:
|
||
LOGGER.error('Received invalid loc "%s" for map "%s".', terr_id, self.map_name)
|
||
return
|
||
if via_convoy not in ('Yes', 'No', '', None):
|
||
LOGGER.error('Received invalid via convoy "%s". Expected "Yes" or "No" or "".', via_convoy)
|
||
return
|
||
|
||
# Extracting locations
|
||
loc_name = CACHE[self.map_name]['ix_to_loc'].get(terr_id, None)
|
||
to_loc_name = CACHE[self.map_name]['ix_to_loc'].get(to_terr_id, None)
|
||
from_loc_name = CACHE[self.map_name]['ix_to_loc'].get(from_terr_id, None)
|
||
|
||
# Building order
|
||
# --- Hold ---
|
||
# {"id": "76", "unitID": "19", "type": "Hold", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
|
||
if order_type == 'Hold':
|
||
self.order_str = '%s %s H' % (unit_type[0], loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Hold',
|
||
'toTerrID': '',
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Move ---
|
||
# {"id": "73", "unitID": "16", "type": "Move", "toTerrID": "25", "fromTerrID": "", "viaConvoy": "Yes",
|
||
# "convoyPath": ["22", "69"]},
|
||
# {"id": "74", "unitID": "17", "type": "Move", "toTerrID": "69", "fromTerrID": "", "viaConvoy": "No"}
|
||
elif order_type == 'Move':
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Move] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# We don't need to set the "convoyPath" property if we are converting from an order_dict
|
||
via_flag = ' VIA' if via_convoy == 'Yes' else ''
|
||
self.order_str = '%s %s - %s%s' % (unit_type[0], loc_name, to_loc_name, via_flag)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Move',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': via_convoy}
|
||
|
||
# --- Support hold ---
|
||
# {"id": "73", "unitID": "16", "type": "Support hold", "toTerrID": "24", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'Support hold':
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Support H] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
self.order_str = '%s %s S %s' % (unit_type[0], loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Support hold',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Support move ---
|
||
# {"id": "73", "unitID": "16", "type": "Support move", "toTerrID": "24", "fromTerrID": "69", "viaConvoy": ""}
|
||
elif order_type == 'Support move':
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Support M] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
if from_loc_name is None:
|
||
LOGGER.error('[Support M] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
self.order_str = '%s %s S %s - %s' % (unit_type[0], loc_name, from_loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Support move',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': from_terr_id,
|
||
'viaConvoy': ''}
|
||
|
||
# --- Convoy ---
|
||
# {"id": "79", "unitID": "22", "type": "Convoy", "toTerrID": "24", "fromTerrID": "20", "viaConvoy": "",
|
||
# "convoyPath": ["20", "69"]}
|
||
elif order_type == 'Convoy':
|
||
if to_loc_name is None:
|
||
LOGGER.error('[Convoy] Received invalid to loc "%s" for map "%s".', to_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
if from_loc_name is None:
|
||
LOGGER.error('[Convoy] Received invalid from loc "%s" for map "%s".', from_terr_id, self.map_name)
|
||
LOGGER.error(order)
|
||
return
|
||
|
||
# We don't need to set the "convoyPath" property if we are converting from an order_dict
|
||
self.order_str = '%s %s C A %s - %s' % (unit_type[0], loc_name, from_loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Convoy',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': from_terr_id,
|
||
'viaConvoy': ''}
|
||
|
||
# --- Retreat ---
|
||
# {"id": "152", "unitID": "18", "type": "Retreat", "toTerrID": "75", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'Retreat':
|
||
if to_loc_name is None:
|
||
return
|
||
self.order_str = '%s %s R %s' % (unit_type[0], loc_name, to_loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Retreat',
|
||
'toTerrID': to_terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Disband ---
|
||
# {"id": "152", "unitID": "18", "type": "Disband", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'Disband':
|
||
self.order_str = '%s %s D' % (unit_type[0], loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Disband',
|
||
'toTerrID': '',
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Build Army ---
|
||
# [{"id": "56", "unitID": null, "type": "Build Army", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
|
||
elif order_type == 'Build Army':
|
||
self.order_str = 'A %s B' % loc_name
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': 'Army',
|
||
'type': 'Build Army',
|
||
'toTerrID': terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Build Fleet ---
|
||
# [{"id": "56", "unitID": null, "type": "Build Fleet", "toTerrID": "37", "fromTerrID": "", "viaConvoy": ""}]
|
||
elif order_type == 'Build Fleet':
|
||
self.order_str = 'F %s B' % loc_name
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': 'Fleet',
|
||
'type': 'Build Fleet',
|
||
'toTerrID': terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# --- Wait / Waive ---
|
||
# [{"id": "56", "unitID": null, "type": "Wait", "toTerrID": "", "fromTerrID": "", "viaConvoy": ""}]
|
||
elif order_type == 'Wait':
|
||
self.order_str = 'WAIVE'
|
||
self.order_dict = {'terrID': None,
|
||
'unitType': '',
|
||
'type': 'Wait',
|
||
'toTerrID': '',
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
# Disband (A phase)
|
||
# {"id": "152", "unitID": null, "type": "Destroy", "toTerrID": "18", "fromTerrID": "", "viaConvoy": ""}
|
||
elif order_type == 'Destroy':
|
||
self.order_str = '%s %s D' % (unit_type[0], loc_name)
|
||
self.order_dict = {'terrID': terr_id,
|
||
'unitType': unit_type,
|
||
'type': 'Destroy',
|
||
'toTerrID': terr_id,
|
||
'fromTerrID': '',
|
||
'viaConvoy': ''}
|
||
|
||
def __bool__(self):
|
||
""" Returns True if an order was parsed, False otherwise """
|
||
return bool(self.order_str != '')
|
||
|
||
def __str__(self):
|
||
""" Returns the string representation of the order """
|
||
return self.order_str
|
||
|
||
def to_string(self):
|
||
""" Returns the string representation of the order """
|
||
return self.order_str
|
||
|
||
def to_norm_string(self):
|
||
""" Returns a normalized order string """
|
||
if self.order_str[-2:] == ' D':
|
||
order_str = '? ' + self.order_str[2:]
|
||
else:
|
||
order_str = self.order_str
|
||
return order_str\
|
||
.replace(' S A ', ' S ')\
|
||
.replace(' S F ', ' S ') \
|
||
.replace(' C A ', ' C ') \
|
||
.replace(' C F ', ' C ') \
|
||
.replace(' VIA', '')
|
||
|
||
def to_dict(self):
|
||
""" Returns the dictionary representation of the order """
|
||
return self.order_dict
|