diff options
author | Satya Ortiz-Gagne <satya.ortiz-gagne@mila.quebec> | 2019-06-06 12:22:05 -0400 |
---|---|---|
committer | Philip Paquette <pcpaquette@gmail.com> | 2019-06-07 20:02:12 -0400 |
commit | 714363dbd87b6426db3f4c449d190a237a54e51c (patch) | |
tree | 8664321962bfbb21a9d661dc753935f7a0f44eeb | |
parent | d0e4f20bcffb11368935603da1205ee7bc167dc9 (diff) |
DAIDE - Added ability to parse clauses
- break_next_group
- add_parentheses / strip_parentheses
- parse_bytes / parse_string
- SingleToken clause
- Power clause
- String clause
- Number clause
- Province clause
- Turn clause
- UnitType clause
- Unit clause
- Order Type clause
- Order clause
-rw-r--r-- | diplomacy/daide/clauses.py | 802 |
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() |