From 6e76389a852f327e98f2b87b22f2a63d977a9470 Mon Sep 17 00:00:00 2001
From: Philip Paquette <pcpaquette@gmail.com>
Date: Sun, 17 Feb 2019 16:18:24 -0500
Subject: Deprecated support for A WWW - XXX - YYY - ZZZ (explicit multi-step
 convoy)

- Order expansion will convert the order to 'A WWW - ZZZ VIA'
- set_orders without expansion will reject the order as an invalid move order
---
 diplomacy/engine/game.py | 191 +++++++++++++++++------------------------------
 1 file changed, 69 insertions(+), 122 deletions(-)

(limited to 'diplomacy')

diff --git a/diplomacy/engine/game.py b/diplomacy/engine/game.py
index 66a4cf9..b639573 100644
--- a/diplomacy/engine/game.py
+++ b/diplomacy/engine/game.py
@@ -1909,7 +1909,6 @@ class Game(Jsonable):
             return None
         word = order.split()
         owner = self._unit_owner(unit)
-        rules = self.rules
 
         # No order
         if not word:
@@ -2086,7 +2085,7 @@ class Game(Jsonable):
         # -------------------------------------------------------------
         # MOVE order
         elif order_type == '-':
-            # Expected format '- xxx' or '- xxx - yyy - zzz'
+            # Expected format '- xxx' or '- xxx VIA'
             if (len(word) & 1 and word[-1] != 'VIA') or (len(word[:-1]) & 1 and word[-1] == 'VIA'):
                 if report:
                     self.error.append(err.GAME_BAD_MOVE_ORDER % (unit, order))
@@ -2098,96 +2097,45 @@ class Game(Jsonable):
                     self.error.append(err.GAME_UNIT_CANT_CONVOY % (unit, order))
                 return None
 
-            # Step through every "- xxx" in the order and ensure the unit can get there at every step
+            # Step through "- xxx" in the order and ensure the unit can get there
             src = unit_loc
-            order_type = 'C-'[len(word) == 2 or (len(word) == 3 and word[-1] == 'VIA')]
-            visit = []
+            offset = 1 if word[-1] == 'VIA' else 0
+            to_loc = word[-1 - offset]
+
+            # Making sure that the syntax is '- xxx'
+            # The old syntax 'A XXX - YYY - ZZZ' is deprecated
+            if len(word) - offset > 2 or word[0] != '-':
+                if report:
+                    self.error.append(err.GAME_BAD_MOVE_ORDER % (unit, order))
+                return None
 
             # Checking that unit is not returning back where it started ...
-            if word[-1] == unit_loc and order_type < 'C':
+            if to_loc == unit_loc:
                 if report:
                     self.error.append(err.GAME_MOVING_UNIT_CANT_RETURN % (unit, order))
                 return None
 
-            # For a multi-step convoy
-            if order_type == 'C':
+            # Move only possible through convoy
+            if not self._abuts(unit_type, src, order_type, to_loc) or word[-1] == 'VIA':
 
                 # Checking that destination is a COAST or PORT ...
-                if self.map.area_type(word[-1]) not in ('COAST', 'PORT'):
+                if self.map.area_type(to_loc) not in ('COAST', 'PORT'):
                     if report:
                         self.error.append(err.GAME_CONVOYING_UNIT_MUST_REACH_COST % (unit, order))
                     return None
 
                 # Making sure that army is not having a specific coast as destination ...
-                if unit_type == 'A' and '/' in word[-1]:
+                if unit_type == 'A' and '/' in to_loc:
                     if report:
                         self.error.append(err.GAME_ARMY_CANT_CONVOY_TO_COAST % (unit, order))
                     return None
 
-            # Making sure that the syntax is '- xxx - yyy - zzz'
-            offset = 1 if word[-1] == 'VIA' else 0
-            if [1 for x in range(0, len(word) - offset, 2) if word[x] != '-']:
-                if report:
-                    self.error.append(err.GAME_BAD_MOVE_ORDER % (unit, order))
-                return None
-
-            # For every location touched
-            ride = word[1:len(word) - offset:2]
-            for num, to_loc in enumerate(ride):
-
-                # Checking that ride is not visited twice ...
-                if to_loc in visit and 'CONVOY_BACK' not in rules:
-                    if report:
-                        self.error.append(err.GAME_CONVOY_UNIT_USED_TWICE % (unit, order))
-                    return None
-                visit += [to_loc]
-
-                # Making sure the last 2 locations touch, and that A/F can convoy through them
-                # pylint: disable=too-many-boolean-expressions
-                if (not self._abuts(unit_type, src, order_type, to_loc)
-                        and not self._has_convoy_path(unit_type, unit_loc, to_loc)
-                        and (len(word) == 2
-                             or unit_type == 'A' and (not self._abuts('F', to_loc, 'S', src))
-                             or (unit_type == 'F'
-                                 and (to_loc[:3].upper() not in
-                                      [abut[:3].upper() for abut in self.map.abut_list(src[:3])])))):
-                    if report:
-                        self.error.append(err.GAME_UNIT_CANT_MOVE_INTO_DEST % (unit, order))
-                    return None
-
-                # If VIA flag set, make sure there is at least a possible path
-                if word[-1] == 'VIA' and not self._has_convoy_path(unit_type, unit_loc, to_loc):
+                # Make sure there is at least a possible path
+                if not self._has_convoy_path(unit_type, unit_loc, to_loc):
                     if report:
                         self.error.append(err.GAME_UNIT_CANT_MOVE_VIA_CONVOY_INTO_DEST % (unit, order))
                     return None
 
-                # If we are at an intermediary location
-                if num < len(ride) - 1:
-
-                    # Trying to portage convoy fleet through water
-                    # or trying to convoy army through LAND or COAST
-                    if (unit_type == 'F'
-                            and ((unit_type == 'A' and self.map.area_type(to_loc) not in ('WATER', 'PORT'))
-                                 or unit_type + self.map.area_type(to_loc) == 'FWATER')):
-                        if report:
-                            self.error.append(err.GAME_BAD_CONVOY_MOVE_ORDER % (unit, order))
-                        return None
-
-                    # Making sure there is a unit there to convoy ...
-                    if not self._unit_owner('AF'[unit_type == 'A'] + ' ' + to_loc):
-                        if report:
-                            self.error.append(err.GAME_CONVOY_THROUGH_NON_EXISTENT_UNIT % (unit, order))
-                        return None
-
-                # Portaging fleets must finish the turn on a coastal location listed in upper-case
-                elif (num
-                      and unit_type == 'F'
-                      and (to_loc not in self.map.loc_abut or self.map.area_type(to_loc) not in ('COAST', 'PORT'))):
-                    if report:
-                        self.error.append(err.GAME_IMPOSSIBLE_CONVOY % (unit, order))
-                    return None
-                src = to_loc
-
         # -------------------------------------------------------------
         # HOLD order
         elif order_type == 'H':
@@ -2216,6 +2164,11 @@ class Game(Jsonable):
         result = self.map.compact(' '.join(word))
         result = self.map.vet(self.map.rearrange(result), 1)
 
+        # If multiple move seps '-' are present, skipping locs after each move separator except the last one
+        count_move_seps = 0
+        total_move_seps = len([1 for x in word if x == '-'])
+        add_via_flag = total_move_seps >= 2
+
         # Removing errors (Negative values)
         final, order = [], ''
         for result_ix, (token, token_type) in enumerate(result):
@@ -2254,8 +2207,15 @@ class Game(Jsonable):
                     continue
                 order += token
 
-            # Treat each move order the same. Eventually we'd want to distinguish between them
+            # Skip locations except after the last move separator
+            elif token_type == LOCATION and 0 < count_move_seps < total_move_seps:
+                continue
+
+            # Only keeping the first move separator. Others are discarded.
             elif token_type == MOVE_SEP:
+                count_move_seps += 1
+                if count_move_seps >= 2:
+                    continue
                 result[result_ix] = '-', token_type
                 order += '-'
 
@@ -2271,6 +2231,10 @@ class Game(Jsonable):
 
             final += [token]
 
+        # If we detected multiple move separated - Adding a final VIA flag
+        if add_via_flag and final[-1] != 'VIA':
+            final += ['VIA']
+
         # Default any fleet move's coastal destination, then we're done
         return self.map.default_coast(final)
 
@@ -3727,59 +3691,42 @@ class Game(Jsonable):
             if word[0] != '-':
                 continue
 
-            # Full convoy path has been specified (e.g. 'A PAR - MAR - NAO - MAO - LON')
             offset = 1 if word[-1] == 'VIA' else 0
-            if len(word) - offset > 2:
-                for convoyer in range(1, len(word) - 1, 2):
-                    convoy_order = self.command.get('AF'[unit[0] == 'A'] + ' ' + word[convoyer])
-                    if convoy_order not in ['C %s - ' % x + word[-1] for x in (unit, unit[2:])]:
-                        if convoy_order:
-                            self.result[unit] += ['no convoy']
-                        else:
-                            self.command[unit] = 'H'
-                        break
-                # List the valid convoys
-                else:
-                    may_convoy[unit] = order.split()
-                    self.convoy_paths[unit] = [[unit[2:]] + word[1::2]]
-
-            # Only src and dest provided
+            def flatten(nested_list):
+                """ Flattens a sublist """
+                return [list_item for sublist in nested_list for list_item in sublist]
+
+            has_via_convoy_flag = 1 if word[-1] == 'VIA' else 0
+            convoying_units = self._get_convoying_units_for_path(unit[0], unit[2:], word[1])
+            possible_paths = self._get_convoy_paths(unit[0],
+                                                    unit[2:],
+                                                    word[1],
+                                                    has_via_convoy_flag,
+                                                    convoying_units)
+
+            # No convoy path - Removing VIA and checking if adjacent
+            if not possible_paths:
+                if has_via_convoy_flag:
+                    self.command[unit] = ' '.join(word[:-1])
+                if not self._abuts(unit[0], unit[2:], 'S', word[1]):
+                    self.result[unit] += ['no convoy']
+
+            # There is a convoy path, remembering the convoyers
             else:
-                def flatten(nested_list):
-                    """ Flattens a sublist """
-                    return [list_item for sublist in nested_list for list_item in sublist]
-
-                has_via_convoy_flag = 1 if word[-1] == 'VIA' else 0
-                convoying_units = self._get_convoying_units_for_path(unit[0], unit[2:], word[1])
-                possible_paths = self._get_convoy_paths(unit[0],
-                                                        unit[2:],
-                                                        word[1],
-                                                        has_via_convoy_flag,
-                                                        convoying_units)
-
-                # No convoy path - Removing VIA and checking if adjacent
-                if not possible_paths:
-                    if has_via_convoy_flag:
-                        self.command[unit] = ' '.join(word[:-1])
-                    if not self._abuts(unit[0], unit[2:], 'S', word[1]):
-                        self.result[unit] += ['no convoy']
-
-                # There is a convoy path, remembering the convoyers
-                else:
-                    self.convoy_paths[unit] = possible_paths
-                    may_convoy.setdefault(unit, [])
-                    for convoyer in convoying_units:
-                        if convoyer[2:] in flatten(possible_paths) and convoyer[2:] not in may_convoy[unit]:
-                            may_convoy[unit] += [convoyer[2:]]
-
-                # Marking all convoys that are not in any path
-                invalid_convoys = convoying_units[:]
-                all_path_locs = list(set(flatten(possible_paths)))
-                for convoy in convoying_units:
-                    if convoy[2:] in all_path_locs:
-                        invalid_convoys.remove(convoy)
-                for convoy in invalid_convoys:
-                    self.result[convoy] = ['no convoy']
+                self.convoy_paths[unit] = possible_paths
+                may_convoy.setdefault(unit, [])
+                for convoyer in convoying_units:
+                    if convoyer[2:] in flatten(possible_paths) and convoyer[2:] not in may_convoy[unit]:
+                        may_convoy[unit] += [convoyer[2:]]
+
+            # Marking all convoys that are not in any path
+            invalid_convoys = convoying_units[:]
+            all_path_locs = list(set(flatten(possible_paths)))
+            for convoy in convoying_units:
+                if convoy[2:] in all_path_locs:
+                    invalid_convoys.remove(convoy)
+            for convoy in invalid_convoys:
+                self.result[convoy] = ['no convoy']
 
         # -----------------------------------------------------------
         # STEP 1B. CANCEL ALL INVALID CONVOY ORDERS
-- 
cgit v1.2.3