aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/daide/clauses.py
diff options
context:
space:
mode:
Diffstat (limited to 'diplomacy/daide/clauses.py')
-rw-r--r--diplomacy/daide/clauses.py802
1 files changed, 802 insertions, 0 deletions
diff --git a/diplomacy/daide/clauses.py b/diplomacy/daide/clauses.py
new file mode 100644
index 0000000..9815527
--- /dev/null
+++ b/diplomacy/daide/clauses.py
@@ -0,0 +1,802 @@
+# ==============================================================================
+# 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/>.
+# ==============================================================================
+""" Daide Clauses - Contains clauses that can be used to build / parse requests and responses """
+from abc import ABCMeta, abstractmethod
+import logging
+from diplomacy.daide import tokens
+from diplomacy.daide.tokens import Token
+
+# Constants
+LOGGER = logging.getLogger(__name__)
+
+def break_next_group(daide_bytes):
+ """ If the next token is a parenthesis, finds its matching closing parenthesis and returns a tuple of the items
+ between parentheses and the items after the closing parenthesis.
+ e.g. bytes for (ENG AMY PAR) MTO NWY would return --> (ENG AMY PAR) + MTO NWY
+ e.g. bytes for ENG AMY PAR would return -> '' + ENG AMY PAR since the byte array does not start with a "("
+
+ :return: A tuple consisting of the parenthesis group and the remaining bytes after the group
+ or an empty byte array and the entire byte array if the byte array does not start with a parenthesis
+ """
+ if not daide_bytes:
+ return b'', b''
+
+ # Finding the matching closing parenthesis
+ pos = 0
+ parentheses_level = 0
+ while True:
+ if daide_bytes[pos:pos + 2] == bytes(tokens.OPE_PAR):
+ parentheses_level += 1
+ elif daide_bytes[pos:pos + 2] == bytes(tokens.CLO_PAR):
+ parentheses_level -= 1
+ if parentheses_level <= 0:
+ break
+ if pos >= len(daide_bytes): # Parentheses don't match - Not returning group
+ pos = 0
+ break
+ pos += 2
+
+ # Returning
+ return (daide_bytes[:pos + 2], daide_bytes[pos + 2:]) if pos else (None, daide_bytes)
+
+def add_parentheses(daide_bytes):
+ """ Add parentheses to a list of bytes """
+ if not daide_bytes:
+ return daide_bytes
+ return bytes(tokens.OPE_PAR) + daide_bytes + bytes(tokens.CLO_PAR)
+
+def strip_parentheses(daide_bytes):
+ """ Removes parentheses from the DAIDE bytes and returns the inner content.
+ The first and last token are expected to be parentheses.
+ """
+ assert daide_bytes[:2] == bytes(tokens.OPE_PAR), 'Expected bytes to start with "("'
+ assert daide_bytes[-2:] == bytes(tokens.CLO_PAR), 'Expected bytes to end wth ")"'
+ return daide_bytes[2:-2]
+
+def parse_bytes(clause_constructor, daide_bytes, on_error='raise'):
+ """ Creates a clause object from a string of bytes
+ :param clause_constructor: The type of clause to build
+ :param daide_bytes: The bytes to use to build this clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: A tuple of 1) the clause object, and 2) the remaining (unparsed) bytes
+ """
+ assert on_error in ('raise', 'warn', 'ignore'), 'Valid values for error are "raise", "warn", "ignore"'
+ clause = clause_constructor()
+ daide_bytes = clause.from_bytes(daide_bytes, on_error=on_error)
+ if not clause.is_valid:
+ return None, daide_bytes
+ return clause, daide_bytes
+
+def parse_string(clause_constructor, string, on_error='raise'):
+ """ Creates a clause object from a string
+ :param clause_constructor: The type of clause to build
+ :param string: The string to use to build this clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The clause object
+ """
+ assert on_error in ('raise', 'warn', 'ignore'), 'Valid values for error are "raise", "warn", "ignore"'
+ clause = clause_constructor()
+ clause.from_string(string, on_error=on_error)
+ if not clause.is_valid:
+ return None
+ return clause
+
+class AbstractClause(metaclass=ABCMeta):
+ """ Abstract version of a DAIDE clause """
+ def __init__(self):
+ """ Constructor """
+ self._is_valid = True
+
+ @property
+ def is_valid(self):
+ """ Indicates if the clause is valid (no errors were triggered) """
+ return self._is_valid
+
+ @abstractmethod
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ raise NotImplementedError()
+
+ @abstractmethod
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ raise NotImplementedError()
+
+ def error(self, on_error, message=''):
+ """ Performs the error action
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :param message: The message to display
+ """
+ assert on_error in ('raise', 'warn', 'ignore'), 'Valid values for error are "raise", "warn", "ignore"'
+ if on_error == 'raise':
+ raise RuntimeError(message)
+ if on_error == 'warn':
+ LOGGER.warning(message)
+ self._is_valid = False
+
+class SingleToken(AbstractClause):
+ """ Extracts a single token
+ e.g. NME
+ """
+ def __init__(self):
+ """ Constructor """
+ super(SingleToken, self).__init__()
+ self._bytes = b''
+ self._str = ''
+
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ return self._bytes
+
+ def __str__(self):
+ """ Return the Diplomacy str """
+ return self._str
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ token_bytes, remaining_bytes = daide_bytes[:2], daide_bytes[2:]
+
+ # Not enough bytes to get a token
+ if not token_bytes:
+ self.error(on_error, 'At least 2 bytes are required to build a token.')
+ return remaining_bytes
+
+ # Getting the token
+ self._bytes = token_bytes
+ self._str = str(Token(from_bytes=token_bytes))
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build this clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ # Not enough bytes to get a token
+ if not string:
+ self.error(on_error, '`string` cannot be empty or None')
+ return
+
+ # Getting the token
+ self._bytes = bytes(Token(from_str=string))
+ self._str = string
+
+class Power(SingleToken):
+ """ Each clause is a power
+ Syntax: ENG
+ """
+ _alias_from_bytes = {'AUS': 'AUSTRIA',
+ 'ENG': 'ENGLAND',
+ 'FRA': 'FRANCE',
+ 'GER': 'GERMANY',
+ 'ITA': 'ITALY',
+ 'RUS': 'RUSSIA',
+ 'TUR': 'TURKEY'}
+ _alias_from_string = {value: key for key, value in _alias_from_bytes.items()}
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ remaining_bytes = super(Power, self).from_bytes(daide_bytes, on_error)
+ self._str = self._alias_from_bytes.get(self._str, self._str)
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build this clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ str_power = self._alias_from_string.get(string, string)
+ super(Power, self).from_string(str_power, on_error)
+
+class String(AbstractClause):
+ """ A string contained between parentheses
+ Syntax (Text)
+ """
+ def __init__(self):
+ """ Constructor """
+ super(String, self).__init__()
+ self._bytes = b''
+ self._str = ''
+
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ return self._bytes
+
+ def __str__(self):
+ """ Return the Diplomacy str """
+ return self._str
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ str_group_bytes, remaining_bytes = break_next_group(daide_bytes)
+
+ # Can't find the string
+ if not str_group_bytes:
+ self.error(on_error, 'Unable to find a set of parentheses to extract the string clause.')
+ return daide_bytes
+
+ # Extract its content
+ nb_bytes = len(str_group_bytes)
+ self._bytes = str_group_bytes
+ self._str = ''.join([str(Token(from_bytes=str_group_bytes[pos:pos + 2])) for pos in range(2, nb_bytes - 2, 2)])
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ self._bytes = add_parentheses(b''.join([bytes(Token(from_str=char)) for char in string]))
+ self._str = string
+
+class Number(AbstractClause):
+ """ A number contained between parentheses
+ Syntax: Number
+ """
+ def __init__(self):
+ """ Constructor """
+ super(Number, self).__init__()
+ self._bytes = b''
+ self._int = 0
+
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ return self._bytes
+
+ def __str__(self):
+ """ Return the Diplomacy str """
+ return str(self._int)
+
+ def __int__(self):
+ """ Return the Diplomacy int """
+ return self._int
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ if not daide_bytes:
+ self.error(on_error, 'Expected at least 1 byte to parse a number')
+ return daide_bytes
+
+ number_bytes, remaining_bytes = daide_bytes[:2], daide_bytes[2:]
+ number_token = Token(from_bytes=number_bytes)
+ if not tokens.is_integer_token(number_token):
+ self.error(on_error, 'The token is not an integer. Got %s' % number_token)
+ return daide_bytes
+
+ # Extract its content
+ self._bytes = number_bytes
+ self._int = int(number_token)
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ self._bytes = bytes(Token(from_int=int(string)))
+ self._int = int(string)
+
+class Province(AbstractClause):
+ """ Each clause is an province token
+ Syntax: ADR
+ (STP ECS)
+ """
+ _alias_from_bytes = {'ECS': '/EC',
+ 'NCS': '/NC',
+ 'SCS': '/SC',
+ 'WCS': '/WC',
+ 'ECH': 'ENG',
+ 'GOB': 'BOT',
+ 'GOL': 'LYO'}
+ _alias_from_string = {value: key for key, value in _alias_from_bytes.items()}
+
+ def __init__(self):
+ """ Constructor """
+ super(Province, self).__init__()
+ self._bytes = b''
+ self._str = ''
+
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ return self._bytes
+
+ def __str__(self):
+ """ Return the Diplomacy str """
+ return self._str
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ province_group_bytes, remaining_bytes = break_next_group(daide_bytes)
+
+ # Is a province with coast
+ # Syntax (STP NCS)
+ if province_group_bytes:
+ self._bytes = province_group_bytes
+
+ province_group_bytes = strip_parentheses(province_group_bytes)
+ province, province_group_bytes = parse_bytes(SingleToken, province_group_bytes, on_error=on_error)
+ coast, province_group_bytes = parse_bytes(SingleToken, province_group_bytes, on_error=on_error)
+
+ if province_group_bytes:
+ self.error(on_error, '{} bytes remaining. Province is malformed'.format(len(province_group_bytes)))
+ return daide_bytes
+
+ str_province = self._alias_from_bytes.get(str(province), str(province))
+ str_coast = self._alias_from_bytes.get(str(coast), str(coast))
+ self._str = str_province + str_coast
+
+ # Is a province with no coast
+ # Syntax: ADR
+ else:
+ province, remaining_bytes = parse_bytes(SingleToken, remaining_bytes, on_error=on_error)
+ self._bytes = bytes(province)
+ self._str = self._alias_from_bytes.get(str(province), str(province))
+
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ province, coast = string.split('/') if '/' in string else [string, None]
+
+ # Province with coast
+ # Syntax: (STP NCS)
+ if province and coast:
+ str_province = self._alias_from_string.get(province, province)
+ str_coast = self._alias_from_string.get('/' + coast, '')
+
+ if not str_coast:
+ self.error(on_error, 'Unknown coast "%s".' % '/' + coast)
+ return
+
+ self._str = str_province + str_coast
+ self._bytes = add_parentheses(bytes(Token(from_str=str_province)) + bytes(Token(from_str=str_coast)))
+
+ # Province without coast
+ # Syntax: ADR
+ else:
+ str_province = self._alias_from_string.get(string, string)
+ self._str = str_province
+ self._bytes = bytes(Token(from_str=str_province))
+
+class Turn(AbstractClause):
+ """ Each clause is a Turn
+ Syntax: (SPR 1901)
+ """
+ _alias_from_bytes = {'AUT': 'F.R',
+ 'FAL': 'F.M',
+ 'SPR': 'S.M',
+ 'SUM': 'S.R',
+ 'WIN': 'W.A'}
+ _alias_from_string = {value: key for key, value in _alias_from_bytes.items()}
+
+ def __init__(self):
+ """ Constructor """
+ super(Turn, self).__init__()
+ self._bytes = b''
+ self._str = ''
+
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ return self._bytes
+
+ def __str__(self):
+ """ Return the Diplomacy str """
+ return self._str
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ turn_group_bytes, remaining_bytes = break_next_group(daide_bytes)
+
+ # Can't find the order
+ if not turn_group_bytes:
+ self.error(on_error, 'Unable to find a set of parentheses to extract the turn clause.')
+ return daide_bytes
+
+ self._bytes = turn_group_bytes
+
+ turn_group_bytes = strip_parentheses(turn_group_bytes)
+ season, turn_group_bytes = parse_bytes(SingleToken, turn_group_bytes, on_error=on_error)
+ year, turn_group_bytes = parse_bytes(Number, turn_group_bytes, on_error=on_error)
+
+ if turn_group_bytes:
+ self.error(on_error, '{} bytes remaining. Turn is malformed'.format(len(turn_group_bytes)))
+ return daide_bytes
+
+ season_alias = self._alias_from_bytes.get(str(season), str(season))
+ self._str = ''.join([season_alias[0], str(year), season_alias[-1]])
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ str_season = self._alias_from_string.get('%s.%s' % (string[0], string[-1]), '')
+ str_year = string[1:-1]
+
+ if not str_season or not str_year:
+ self.error(on_error, 'Unknown season and/or year "%s".' % string)
+ return
+
+ self._str = string
+ self._bytes = add_parentheses(bytes(Token(from_str=str_season)) + bytes(Token(from_int=int(str_year))))
+
+class UnitType(SingleToken):
+ """ Each clause is an season token
+ Syntax: AMY
+ """
+ _alias_from_bytes = {'AMY': 'A',
+ 'FLT': 'F'}
+ _alias_from_string = {value: key for key, value in _alias_from_bytes.items()}
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ remaining_bytes = super(UnitType, self).from_bytes(daide_bytes, on_error)
+ self._str = self._alias_from_bytes.get(self._str, self._str)
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build this clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ str_unit_type = self._alias_from_string.get(string, '')
+ if not str_unit_type:
+ self.error(on_error, 'Unknown unit type "%s"' % string)
+ self._str = string
+ self._bytes = bytes(Token(from_str=str_unit_type))
+
+class Unit(AbstractClause):
+ """ Each clause is an army or fleet
+ Syntax: (ITA AMY TUN)
+ """
+ _UNK = 'UNO' # Unknown power
+
+ def __init__(self):
+ """ Constructor """
+ super(Unit, self).__init__()
+ self._bytes = b''
+ self._str = ''
+ self._power_name = None
+
+ @property
+ def power_name(self):
+ """ The power name """
+ return self._power_name
+
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ return self._bytes
+
+ def __str__(self):
+ """ Return the Diplomacy str """
+ return self._str
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ unit_group_bytes, remaining_bytes = break_next_group(daide_bytes)
+
+ # Can't find the order
+ if not unit_group_bytes:
+ self.error(on_error, 'Unable to find a set of parentheses to extract the order clause.')
+ return daide_bytes
+
+ # Extract its content
+ self._bytes = unit_group_bytes
+
+ unit_group_bytes = strip_parentheses(unit_group_bytes)
+ power, unit_group_bytes = parse_bytes(Power, unit_group_bytes, on_error=on_error)
+ unit_type, unit_group_bytes = parse_bytes(UnitType, unit_group_bytes, on_error=on_error)
+ province, unit_group_bytes = parse_bytes(Province, unit_group_bytes, on_error=on_error)
+
+ if unit_group_bytes:
+ self.error(on_error, '{} bytes remaining. Order is malformed'.format(len(unit_group_bytes)))
+ return daide_bytes
+
+ self._power_name = str(power)
+ self._str = ' '.join([str(unit_type), str(province)])
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ words = string.split()
+
+ # Checking number of words available
+ if len(words) == 2:
+ str_power = self._UNK
+ str_unit_type, str_province = words
+ elif len(words) == 3:
+ str_power, str_unit_type, str_province = words
+ else:
+ self.error(on_error, 'Expected 2 or 3 words (e.g. "A PAR" or "FRANCE A PAR").')
+ return
+
+ # Parsing
+ power = parse_string(Power, str_power, on_error=on_error)
+ unit_type = parse_string(UnitType, str_unit_type, on_error=on_error)
+ province = parse_string(Province, str_province, on_error=on_error)
+
+ self._power_name = str(power)
+ self._str = ' '.join([str(unit_type), str(province)])
+ self._bytes = add_parentheses(bytes(power) + bytes(unit_type) + bytes(province))
+
+class OrderType(SingleToken):
+ """ Each clause is an order token
+ Syntax: SUB
+ """
+ _alias_from_bytes = {'HLD': 'H',
+ 'MTO': '-',
+ 'SUP': 'S',
+ 'CVY': 'C',
+ 'CTO': '-',
+ 'VIA': 'VIA',
+ 'RTO': 'R',
+ 'DSB': 'D',
+ 'BLD': 'B',
+ 'REM': 'D',
+ 'WVE': 'WAIVE'}
+ _alias_from_string = {'H': 'HLD',
+ '-': 'MTO',
+ 'S': 'SUP',
+ 'C': 'CVY',
+ 'VIA': 'VIA',
+ 'R': 'RTO',
+ 'D': 'REM',
+ 'B': 'BLD',
+ 'WAIVE': 'WVE'}
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ remaining_bytes = super(OrderType, self).from_bytes(daide_bytes, on_error)
+ self._str = self._alias_from_bytes.get(self._str, self._str)
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build this clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ str_order_type = self._alias_from_string.get(string, string)
+ super(OrderType, self).from_string(str_order_type, on_error)
+
+def parse_order_to_bytes(phase_type, order_split):
+ """ Builds an order clause from a byte array
+ :param phase_type: The game phase
+ :param order_split: An instance of diplomacy.utils.subject_split.OrderSplit
+ :return: The order clause's bytes
+ """
+ buffer = []
+
+ # FRANCE WAIVE
+ if len(order_split) == 1:
+ words = order_split.order_type.split()
+ buffer.append(parse_string(Power, words.pop(0)))
+ buffer.append(parse_string(OrderType, words.pop(0)))
+ else:
+ buffer.append(parse_string(Unit, order_split.unit))
+
+ # FRANCE F IRI [-] MAO
+ # FRANCE A IRI [-] MAO VIA
+ if order_split.order_type == '-':
+ # FRANCE A IRI - MAO VIA
+ if order_split.via_flag:
+ buffer.append(Token(tokens.CTO))
+ else:
+ buffer.append(Token(tokens.MTO))
+ # FRANCE A IRO [D]
+ elif order_split.order_type == 'D':
+ if phase_type == 'R':
+ buffer.append(Token(tokens.DSB))
+ elif phase_type == 'A':
+ buffer.append(Token(tokens.REM))
+ # FRANCE A LON [H]
+ # FRANCE A WAL [S] FRANCE F LON
+ # FRANCE A WAL [S] FRANCE F MAO - IRI
+ # FRANCE F NWG [C] FRANCE A NWY - EDI
+ # FRANCE A IRO [R] MAO
+ # FRANCE A LON [B]
+ # FRANCE F LIV [B]
+ else:
+ buffer.append(parse_string(OrderType, order_split.order_type))
+
+ # FRANCE A WAL S [FRANCE F LON]
+ # FRANCE A WAL S [FRANCE F MAO] - IRI
+ # FRANCE F NWG C [FRANCE A NWY] - EDI
+ if order_split.supported_unit:
+ buffer.append(parse_string(Unit, order_split.supported_unit))
+
+ # FRANCE A WAL S FRANCE F MAO [- IRI]
+ # FRANCE F NWG C FRANCE A NWY [- EDI]
+ if order_split.support_order_type:
+ # FRANCE A WAL S FRANCE F MAO - IRI
+ if order_split.order_type == 'S':
+ buffer.append(Token(tokens.MTO))
+ buffer.append(parse_string(Province, order_split.destination[:3]))
+ else:
+ buffer.append(Token(tokens.CTO))
+ buffer.append(parse_string(Province, order_split.destination))
+ # FRANCE F IRI - [MAO]
+ # FRANCE A IRI - [MAO] VIA
+ # FRANCE A IRO R [MAO]
+ elif order_split.destination:
+ buffer.append(parse_string(Province, order_split.destination))
+
+ # FRANCE A IRI - MAO [VIA]
+ if order_split.via_flag:
+ buffer.append(parse_string(OrderType, order_split.via_flag))
+
+ return b''.join([bytes(clause) for clause in buffer])
+
+class Order(AbstractClause):
+ """ Each clause is an order
+ Syntax: ((power unit_type location) order_type province)
+ """
+ def __init__(self):
+ """ Constructor """
+ super(Order, self).__init__()
+ self._bytes = b''
+ self._str = ''
+ self._power_name = None
+
+ @property
+ def power_name(self):
+ """ The power name """
+ return self._power_name
+
+ def __bytes__(self):
+ """ Define the DAIDE bytes representation """
+ return self._bytes
+
+ def __str__(self):
+ """ Return the Diplomacy str """
+ return self._str
+
+ def from_bytes(self, daide_bytes, on_error='raise'):
+ """ Builds the clause from a byte array
+ :param daide_bytes: The bytes to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ :return: The remaining (unparsed) bytes
+ """
+ order_group_bytes, remaining_bytes = break_next_group(daide_bytes)
+
+ # Can't find the order
+ if not order_group_bytes:
+ self.error(on_error, 'Unable to find a set of parentheses to extract the order clause.')
+ return daide_bytes
+
+ # Extract its content
+ self._bytes = order_group_bytes
+
+ # Parsing the unit group (or just the power)
+ order_group_bytes = strip_parentheses(order_group_bytes)
+ unit, order_group_bytes = parse_bytes(Unit, order_group_bytes, on_error='ignore')
+ power = None
+ if not unit:
+ power, order_group_bytes = parse_bytes(Power, order_group_bytes, on_error='ignore')
+
+ order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error=on_error)
+ order_type_str = str(order_type)
+
+ if order_type_str == 'WAIVE':
+ str_buffer = [order_type_str]
+
+ elif order_type_str:
+ # Hold, Disband
+ str_buffer = [str(unit), order_type_str]
+
+ # Move order
+ if order_type_str == '-':
+ province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error)
+ str_buffer += [str(province)]
+ second_order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error='ignore')
+ if str(second_order_type) == 'VIA':
+ str_buffer += [str(second_order_type)]
+ province_list, order_group_bytes = break_next_group(order_group_bytes)
+ del province_list
+
+ # Support
+ elif order_type_str == 'S':
+ other_unit, order_group_bytes = parse_bytes(Unit, order_group_bytes, on_error=on_error)
+ str_buffer += [str(other_unit)]
+ second_order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error='ignore')
+ if str(second_order_type) == '-':
+ province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error)
+ str_buffer += [str(second_order_type), str(province)]
+
+ # Convoy
+ elif order_type_str == 'C':
+ other_unit, order_group_bytes = parse_bytes(Unit, order_group_bytes, on_error=on_error)
+ second_order_type, order_group_bytes = parse_bytes(OrderType, order_group_bytes, on_error=on_error)
+ province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error)
+ str_buffer += [str(other_unit), str(second_order_type), str(province)]
+
+ # Retreat
+ elif order_type_str == 'R':
+ province, order_group_bytes = parse_bytes(Province, order_group_bytes, on_error=on_error)
+ str_buffer += [str(province)]
+
+ else:
+ self.error(on_error, 'Unable to find a unit, a power or an order to build the order clause')
+ return daide_bytes
+
+ if order_group_bytes:
+ self.error(on_error, '{} bytes remaining. Order is malformed'.format(len(order_group_bytes)))
+ return daide_bytes
+
+ self._power_name = str(power) if power else unit.power_name
+ self._str = ' '.join(str_buffer)
+ return remaining_bytes
+
+ def from_string(self, string, on_error='raise'):
+ """ Builds the clause from a string
+ :param string: The string to use to build the clause
+ :param on_error: The action to take when an error is encountered ('raise', 'warn', 'ignore')
+ """
+ raise NotImplementedError()