Source code for diplomacy.daide.notifications

# ==============================================================================
# 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

[docs]class DaideNotification: """ Represents a DAIDE response. """
[docs] 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)
[docs] def to_bytes(self): """ Returning the bytes representation of the response """ return bytes(self)
[docs] def to_string(self): """ Returning the string representation of the response """ return str(self)
[docs]class MapNameNotification(DaideNotification): """ Represents a MAP DAIDE response. Sends the name of the current map to the client. Syntax: :: MAP ('name') """
[docs] 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))
[docs]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 """
[docs] 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))
[docs]class SupplyCenterNotification(DaideNotification): """ Represents a SCO DAIDE notification. Sends the current supply centre ownership. Syntax: :: SCO (power centre centre ...) (power centre centre ...) ... """
[docs] 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])
[docs]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 ...) """
[docs] 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)
[docs]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) """
[docs] 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)))
[docs]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. """
[docs] 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))
[docs]class TimeToDeadlineNotification(DaideNotification): """ Represents a TME DAIDE response. Sends the time to the next deadline. Syntax: :: TME (seconds) """
[docs] 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)))
[docs]class PowerInCivilDisorderNotification(DaideNotification): """ Represents a CCD DAIDE response. Sends the name of the power in civil disorder. Syntax: :: CCD (power) """
[docs] 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))
[docs]class PowerIsEliminatedNotification(DaideNotification): """ Represents a OUT DAIDE response. Sends the name of the power eliminated. Syntax: :: OUT (power) """
[docs] 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))
[docs]class DrawNotification(DaideNotification): """ Represents a DRW DAIDE response. Indicates that the game has ended due to a draw Syntax: :: DRW """
[docs] def __init__(self, **kwargs): """ Builds the response """ super(DrawNotification, self).__init__(**kwargs) self._bytes = bytes(tokens.DRW)
[docs]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) """
[docs] 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]])
[docs]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) """
[docs] 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))
[docs]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 """
[docs] 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)
[docs]class TurnOffNotification(DaideNotification): """ Represents an OFF DAIDE response. Requests a client to exit Syntax: :: OFF """
[docs] 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