aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/daide/notifications.py
diff options
context:
space:
mode:
authorSatya Ortiz-Gagne <satya.ortiz-gagne@mila.quebec>2019-06-10 10:12:45 -0400
committerPhilip Paquette <pcpaquette@gmail.com>2019-06-14 15:08:29 -0400
commit4979f43baba8c7377471bbf65e7b5c003bf65406 (patch)
treec7c188edfbbb2a061a50ce0ec976b86d079e0dff /diplomacy/daide/notifications.py
parenta47a787bbc15fc01a20fef7df05437fd3ce7eae8 (diff)
DAIDE - Implemented notifications and notification_managers
- MAP = MapNameNotification - HLO = HelloNotification - SCO = SupplyCenterNotification - NOW = CurrentPositionNotification - MIS = MissingOrdersNotification - ORD = OrderResultNotification - TME = TimeToDeadlineNotification - CCD = PowerInCivilDisorderNotification - OUT = PowerIsEliminatedNotification - DRW = DrawNotification - FRM = MessageFromNotification - SLO = SoloNotification - SMR = SummaryNotification - OFF = TurnOffNotification
Diffstat (limited to 'diplomacy/daide/notifications.py')
-rw-r--r--diplomacy/daide/notifications.py449
1 files changed, 449 insertions, 0 deletions
diff --git a/diplomacy/daide/notifications.py b/diplomacy/daide/notifications.py
new file mode 100644
index 0000000..e9f6366
--- /dev/null
+++ b/diplomacy/daide/notifications.py
@@ -0,0 +1,449 @@
+# ==============================================================================
+# 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 Notifications - Contains a list of responses sent by the server to the client """
+from diplomacy import Map
+from diplomacy.daide.clauses import String, Power, Province, Turn, Unit, add_parentheses, strip_parentheses, \
+ parse_string
+from diplomacy.daide import tokens
+from diplomacy.daide.tokens import Token
+from diplomacy.daide.utils import bytes_to_str, str_to_bytes
+
+class DaideNotification():
+ """ Represents a DAIDE response. """
+ def __init__(self, **kwargs):
+ """ Constructor """
+ del kwargs # Unused kwargs
+ self._bytes = b''
+ self._str = ''
+
+ def __bytes__(self):
+ """ Returning the bytes representation of the response """
+ return self._bytes
+
+ def __str__(self):
+ """ Returning the string representation of the response """
+ return bytes_to_str(self._bytes)
+
+ def to_bytes(self):
+ """ Returning the bytes representation of the response """
+ return bytes(self)
+
+ def to_string(self):
+ """ Returning the string representation of the response """
+ return str(self)
+
+class MapNameNotification(DaideNotification):
+ """ Represents a MAP DAIDE response. Sends the name of the current map to the client.
+ Syntax:
+ MAP ('name')
+ """
+ def __init__(self, map_name, **kwargs):
+ """ Builds the response
+ :param map_name: String. The name of the current map.
+ """
+ super(MapNameNotification, self).__init__(**kwargs)
+ self._bytes = bytes(tokens.MAP) \
+ + bytes(parse_string(String, map_name))
+
+class HelloNotification(DaideNotification):
+ """ Represents a HLO DAIDE response. Sends the power to be played by the client with the passcode to rejoin the
+ game and the details of the game.
+ Syntax:
+ HLO (power) (passcode) (variant) (variant) ...
+ Variant syntax:
+ LVL n # Level of the syntax accepted
+ MTL seconds # Movement time limit
+ RTL seconds # Retreat time limit
+ BTL seconds # Build time limit
+ DSD # Disables the time limit when a client disconects
+ AOA # Any orders accepted
+ LVL 10:
+ Variant syntax:
+ PDA # Accept partial draws
+ NPR # No press during retreat phases
+ NPB # No press during build phases
+ PTL seconds # Press time limit
+ """
+ def __init__(self, power_name, passcode, level, deadline, rules, **kwargs):
+ """ Builds the response
+ :param power_name: The name of the power being played.
+ :param passcode: Integer. A passcode to rejoin the game.
+ :param level: Integer. The daide syntax level of the game
+ :param deadline: Integer. The number of seconds per turn (0 to disable)
+ :param rules: The list of game rules.
+ """
+ super(HelloNotification, self).__init__(**kwargs)
+ power = parse_string(Power, power_name)
+ passcode = Token(from_int=passcode)
+
+ if 'NO_PRESS' in rules:
+ level = 0
+ variants = add_parentheses(bytes(tokens.LVL) + bytes(Token(from_int=level)))
+
+ if deadline > 0:
+ variants += add_parentheses(bytes(tokens.MTL) + bytes(Token(from_int=deadline)))
+ variants += add_parentheses(bytes(tokens.RTL) + bytes(Token(from_int=deadline)))
+ variants += add_parentheses(bytes(tokens.BTL) + bytes(Token(from_int=deadline)))
+
+ if 'NO_CHECK' in rules:
+ variants += add_parentheses(bytes(tokens.AOA))
+
+ self._bytes = bytes(tokens.HLO) \
+ + add_parentheses(bytes(power)) \
+ + add_parentheses(bytes(passcode)) \
+ + add_parentheses(bytes(variants))
+
+class SupplyCenterNotification(DaideNotification):
+ """ Represents a SCO DAIDE notification. Sends the current supply centre ownership.
+ Syntax:
+ SCO (power centre centre ...) (power centre centre ...) ...
+ """
+ def __init__(self, powers_centers, map_name, **kwargs):
+ """ Builds the notification
+ :param powers_centers: A dict of {power_name: centers} objects
+ :param map_name: The name of the map
+ """
+ super(SupplyCenterNotification, self).__init__(**kwargs)
+ remaining_scs = Map(map_name).scs[:]
+ all_powers_bytes = []
+
+ # Parsing each power
+ for power_name in sorted(powers_centers):
+ centers = sorted(powers_centers[power_name])
+ power_clause = parse_string(Power, power_name)
+ power_bytes = bytes(power_clause)
+
+ for center in centers:
+ sc_clause = parse_string(Province, center)
+ power_bytes += bytes(sc_clause)
+ remaining_scs.remove(center)
+
+ all_powers_bytes += [power_bytes]
+
+ # Parsing unowned center
+ uno_token = tokens.UNO
+ power_bytes = bytes(uno_token)
+
+ for center in remaining_scs:
+ sc_clause = parse_string(Province, center)
+ power_bytes += bytes(sc_clause)
+
+ all_powers_bytes += [power_bytes]
+
+ # Storing full response
+ self._bytes = bytes(tokens.SCO) \
+ + b''.join([add_parentheses(power_bytes) for power_bytes in all_powers_bytes])
+
+class CurrentPositionNotification(DaideNotification):
+ """ Represents a NOW DAIDE notification. Sends the current turn, and the current unit positions.
+ Syntax:
+ NOW (turn) (unit) (unit) ...
+ Unit syntax:
+ power unit_type province
+ power unit_type province MRT (province province ...)
+ """
+ def __init__(self, phase_name, powers_units, powers_retreats, **kwargs):
+ """ Builds the notification
+ :param phase_name: The name of the current phase (e.g. 'S1901M')
+ :param powers: A list of `diplomacy.engine.power.Power` objects
+ """
+ super(CurrentPositionNotification, self).__init__(**kwargs)
+ units_bytes_buffer = []
+
+ # Turn
+ turn_clause = parse_string(Turn, phase_name)
+
+ # Units
+ for power_name, units in sorted(powers_units.items()):
+ # Regular units
+ for unit in units:
+ unit_clause = parse_string(Unit, '%s %s' % (power_name, unit))
+ units_bytes_buffer += [bytes(unit_clause)]
+
+ # Dislodged units
+ for unit, retreat_provinces in sorted(powers_retreats[power_name].items()):
+ unit_clause = parse_string(Unit, '%s %s' % (power_name, unit))
+ retreat_clauses = [parse_string(Province, province) for province in retreat_provinces]
+ units_bytes_buffer += [add_parentheses(strip_parentheses(bytes(unit_clause))
+ + bytes(tokens.MRT)
+ + add_parentheses(b''.join([bytes(province)
+ for province in retreat_clauses])))]
+
+ # Storing full response
+ self._bytes = bytes(tokens.NOW) + bytes(turn_clause) + b''.join(units_bytes_buffer)
+
+class MissingOrdersNotification(DaideNotification):
+ """ Represents a MIS DAIDE response. Sends the list of unit for which an order is missing or indication about
+ required disbands or builds.
+ Syntax:
+ MIS (unit) (unit) ...
+ MIS (unit MRT (province province ...)) (unit MRT (province province ...)) ...
+ MIS (number)
+ """
+ def __init__(self, phase_name, power, **kwargs):
+ """ Builds the response
+ :param phase_name: The name of the current phase (e.g. 'S1901M')
+ :param power: The power to check for missing orders
+ :type power: diplomacy.engine.power.Power
+ """
+ super(MissingOrdersNotification, self).__init__(**kwargs)
+ assert phase_name[-1] in 'MRA', 'Invalid phase "%s"' & phase_name
+ {'M': self._build_movement_phase,
+ 'R': self._build_retreat_phase,
+ 'A': self._build_adjustment_phase}[phase_name[-1]](power)
+
+ def _build_movement_phase(self, power):
+ """ Builds the missing orders response for a movement phase """
+ units_with_no_order = [unit for unit in power.units]
+
+ # Removing units for which we have orders
+ for key, value in power.orders.items():
+ unit = key # Regular game {e.g. 'A PAR': '- BUR')
+ if key[0] in 'RIO': # No-check game (key is INVALID, ORDER x, REORDER x)
+ unit = ' '.join(value.split()[:2])
+ if unit in units_with_no_order:
+ units_with_no_order.remove(unit)
+
+ # Storing full response
+ self._bytes = bytes(tokens.MIS) + \
+ b''.join([bytes(parse_string(Unit, '%s %s' % (power.name, unit)))
+ for unit in units_with_no_order])
+
+ def _build_retreat_phase(self, power):
+ """ Builds the missing orders response for a retreat phase """
+ units_bytes_buffer = []
+
+ units_with_no_order = {unit: retreat_provinces for unit, retreat_provinces in power.retreats.items()}
+
+ # Removing units for which we have orders
+ for key, value in power.orders.items():
+ unit = key # Regular game {e.g. 'A PAR': '- BUR')
+ if key[0] in 'RIO': # No-check game (key is INVALID, ORDER x, REORDER x)
+ unit = ' '.join(value.split()[:2])
+ if unit in units_with_no_order:
+ del units_with_no_order[unit]
+
+ for unit, retreat_provinces in sorted(units_with_no_order.items(),
+ key=lambda key_val: key_val[0].split()[-1]):
+ unit_clause = parse_string(Unit, '%s %s' % (power.name, unit))
+ retreat_clauses = [parse_string(Province, province)
+ for province in retreat_provinces]
+ units_bytes_buffer += [add_parentheses(strip_parentheses(bytes(unit_clause))
+ + bytes(tokens.MRT)
+ + add_parentheses(b''.join([bytes(province)
+ for province in retreat_clauses])))]
+
+ self._bytes = bytes(tokens.MIS) + b''.join(units_bytes_buffer)
+
+ def _build_adjustment_phase(self, power):
+ """ Builds the missing orders response for a build phase """
+ disbands_status = len(power.units) - len(power.centers)
+
+ if disbands_status < 0:
+ available_homes = power.homes[:]
+
+ # Removing centers for which it's impossible to build
+ for unit in [unit.split() for unit in power.units]:
+ province = unit[1]
+ if province in available_homes:
+ available_homes.remove(province)
+
+ disbands_status = max(-len(available_homes), disbands_status)
+
+ self._bytes += bytes(tokens.MIS) + add_parentheses(bytes(Token(from_int=disbands_status)))
+
+class OrderResultNotification(DaideNotification):
+ """ Represents a ORD DAIDE response. Sends the result of an order after the turn has been processed.
+ Syntax:
+ ORD (turn) (order) (result)
+ ORD (turn) (order) (result RET)
+ Result syntax:
+ SUC # Order succeeded (can apply to any order).
+ BNC # Move bounced (only for MTO, CTO or RTO orders).
+ CUT # Support cut (only for SUP orders).
+ DSR # Move via convoy failed due to dislodged convoying fleet (only for CTO orders).
+ NSO # No such order (only for SUP, CVY or CTO orders).
+ RET # Unit was dislodged and must retreat.
+ """
+ def __init__(self, phase_name, order_bytes, results, **kwargs):
+ """ Builds the response
+ :param phase_name: The name of the current phase (e.g. 'S1901M')
+ :param order_bytes: The bytes received for the order
+ :param results: An array containing the error codes.
+ """
+ super(OrderResultNotification, self).__init__(**kwargs)
+ turn_clause = parse_string(Turn, phase_name)
+ if not results or 0 in results: # Order success response
+ result_clause = tokens.SUC
+ else: # Generic order failure response
+ result_clause = tokens.NSO
+
+ self._bytes = bytes(tokens.ORD) \
+ + bytes(turn_clause) \
+ + add_parentheses(order_bytes) \
+ + add_parentheses(bytes(result_clause))
+
+class TimeToDeadlineNotification(DaideNotification):
+ """ Represents a TME DAIDE response. Sends the time to the next deadline.
+ Syntax:
+ TME (seconds)
+ """
+ def __init__(self, seconds, **kwargs):
+ """ Builds the response
+ :param seconds: Integer. The number of seconds before deadline
+ """
+ super(TimeToDeadlineNotification, self).__init__(**kwargs)
+ self._bytes = bytes(tokens.TME) + add_parentheses(bytes(tokens.Token(from_int=seconds)))
+
+class PowerInCivilDisorderNotification(DaideNotification):
+ """ Represents a CCD DAIDE response. Sends the name of the power in civil disorder.
+ Syntax:
+ CCD (power)
+ """
+ def __init__(self, power_name, **kwargs):
+ """ Builds the response
+ :param power_name: The name of the power being played.
+ """
+ super(PowerInCivilDisorderNotification, self).__init__(**kwargs)
+ power = parse_string(Power, power_name)
+ self._bytes = bytes(tokens.CCD) + add_parentheses(bytes(power))
+
+class PowerIsEliminatedNotification(DaideNotification):
+ """ Represents a OUT DAIDE response. Sends the name of the power eliminated.
+ Syntax:
+ OUT (power)
+ """
+ def __init__(self, power_name, **kwargs):
+ """ Builds the response
+ :param power_name: The name of the power being played.
+ """
+ super(PowerIsEliminatedNotification, self).__init__(**kwargs)
+ power = parse_string(Power, power_name)
+ self._bytes = bytes(tokens.OUT) + add_parentheses(bytes(power))
+
+class DrawNotification(DaideNotification):
+ """ Represents a DRW DAIDE response. Indicates that the game has ended due to a draw
+ Syntax:
+ DRW
+ """
+ def __init__(self, **kwargs):
+ """ Builds the response
+ """
+ super(DrawNotification, self).__init__(**kwargs)
+ self._bytes = bytes(tokens.DRW)
+
+class MessageFromNotification(DaideNotification):
+ """ Represents a FRM DAIDE response. Indicates that the game has ended due to a draw
+ Syntax:
+ FRM (power) (power power ...) (press_message)
+ FRM (power) (power power ...) (reply)
+ """
+ def __init__(self, from_power_name, to_power_names, message, **kwargs):
+ """ Builds the response
+ """
+ super(MessageFromNotification, self).__init__(**kwargs)
+
+ from_power_clause = bytes(parse_string(Power, from_power_name))
+ to_powers_clause = b''.join([bytes(parse_string(Power, power_name)) for power_name in to_power_names])
+ message_clause = str_to_bytes(message)
+
+ self._bytes = bytes(tokens.FRM) \
+ + b''.join([add_parentheses(clause)
+ for clause in [from_power_clause, to_powers_clause, message_clause]])
+
+class SoloNotification(DaideNotification):
+ """ Represents a SLO DAIDE response. Indicates that the game has ended due to a solo by the specified power
+ Syntax:
+ SLO (power)
+ """
+ def __init__(self, power_name, **kwargs):
+ """ Builds the response
+ :param power_name: The name of the power being solo.
+ """
+ super(SoloNotification, self).__init__(**kwargs)
+ power = parse_string(Power, power_name)
+ self._bytes = bytes(tokens.SLO) + add_parentheses(bytes(power))
+
+class SummaryNotification(DaideNotification):
+ """ Represents a SMR DAIDE response. Sends the summary for each power at the end of the game
+ Syntax:
+ SMR (turn) (power_summary) ...
+ power_summary syntax:
+ power ('name') ('version') number_of_centres
+ power ('name') ('version') number_of_centres year_of_elimination
+ """
+ def __init__(self, phase_name, powers, daide_users, years_of_elimination, **kwargs):
+ """ Builds the Notification """
+ super(SummaryNotification, self).__init__(**kwargs)
+ powers_smrs_clause = []
+
+ # Turn
+ turn_clause = parse_string(Turn, phase_name)
+
+ for power, daide_user, year_of_elimination in zip(powers, daide_users, years_of_elimination):
+ power_smr_clause = []
+
+ name = daide_user.client_name if daide_user else power.get_controller()
+ version = daide_user.client_version if daide_user else 'v0.0.0'
+
+ power_name_clause = bytes(parse_string(Power, power.name))
+ power_smr_clause.append(power_name_clause)
+
+ # (name)
+ name_clause = bytes(parse_string(String, name))
+ power_smr_clause.append(name_clause)
+
+ # (version)
+ version_clause = bytes(parse_string(String, version))
+ power_smr_clause.append(version_clause)
+
+ number_of_centres_clause = bytes(Token(from_int=len(power.centers)))
+ power_smr_clause.append(number_of_centres_clause)
+
+ if not power.centers:
+ year_of_elimination_clause = bytes(Token(from_int=year_of_elimination))
+ power_smr_clause.append(year_of_elimination_clause)
+
+ power_smr_clause = add_parentheses(b''.join(power_smr_clause))
+ powers_smrs_clause.append(power_smr_clause)
+
+ self._bytes = bytes(tokens.SMR) + bytes(turn_clause) + b''.join(powers_smrs_clause)
+
+class TurnOffNotification(DaideNotification):
+ """ Represents an OFF DAIDE response. Requests a client to exit
+ Syntax:
+ OFF
+ """
+ def __init__(self, **kwargs):
+ """ Builds the response """
+ super(TurnOffNotification, self).__init__(**kwargs)
+ self._bytes = bytes(tokens.OFF)
+
+MAP = MapNameNotification
+HLO = HelloNotification
+SCO = SupplyCenterNotification
+NOW = CurrentPositionNotification
+MIS = MissingOrdersNotification
+ORD = OrderResultNotification
+TME = TimeToDeadlineNotification
+CCD = PowerInCivilDisorderNotification
+OUT = PowerIsEliminatedNotification
+DRW = DrawNotification
+FRM = MessageFromNotification
+SLO = SoloNotification
+SMR = SummaryNotification
+OFF = TurnOffNotification