aboutsummaryrefslogtreecommitdiff
path: root/diplomacy/engine
diff options
context:
space:
mode:
Diffstat (limited to 'diplomacy/engine')
-rw-r--r--diplomacy/engine/game.py616
-rw-r--r--diplomacy/engine/map.py229
-rw-r--r--diplomacy/engine/message.py43
-rw-r--r--diplomacy/engine/power.py99
-rw-r--r--diplomacy/engine/renderer.py65
5 files changed, 654 insertions, 398 deletions
diff --git a/diplomacy/engine/game.py b/diplomacy/engine/game.py
index 5f0bb2e..fe4488e 100644
--- a/diplomacy/engine/game.py
+++ b/diplomacy/engine/game.py
@@ -16,6 +16,7 @@
# ==============================================================================
# -*- coding: utf-8 -*-
""" Game
+
- Contains the game engine
"""
# pylint: disable=too-many-lines
@@ -37,7 +38,7 @@ from diplomacy.engine.renderer import Renderer
from diplomacy.utils import PriorityDict, common, exceptions, parsing, strings
from diplomacy.utils.jsonable import Jsonable
from diplomacy.utils.sorted_dict import SortedDict
-from diplomacy.utils.constants import OrderSettings
+from diplomacy.utils.constants import OrderSettings, DEFAULT_GAME_RULES
from diplomacy.utils.game_phase_data import GamePhaseData, MESSAGES_TYPE
# Constants
@@ -45,126 +46,176 @@ UNDETERMINED, POWER, UNIT, LOCATION, COAST, ORDER, MOVE_SEP, OTHER = 0, 1, 2, 3,
LOGGER = logging.getLogger(__name__)
class Game(Jsonable):
- """
- - combat - Dictionary of dictionaries containing the strength of every attack on a location (including units
- who don't count toward dislodgment)
- - Format: {loc: attack_strength: [ ['src loc', [support loc] ]}
- e.g. { 'MUN': { 1 : [ ['A MUN', [] ], ['A RUH', [] ] ], 2 : [ ['A SIL', ['A BOH'] ] ] } }
- MUN is holding, being attack without support from RUH and being attacked with support from SIL
- (S from BOH)
- - command - Contains the list of finalized orders to be processed (same format as orders, but without .order)
- e.g. {'A PAR': '- A MAR'}
- - controlled_powers: for client games only. List of powers currently controlled by associated client user.
- - convoy_paths - Contains the list of remaining convoys path for each convoyed unit to reach their destination
- Note: This is used to see if there are still active convoy paths remaining.
- Note: This also include the start and ending location
- e.g. {'A PAR': [ ['PAR', 'ION','NAO', 'MAR], ['PAR', 'ION', 'MAR'] ], ... }
- - convoy_paths_possible - Contains the list of possible convoy paths given the current fleet locations or None
- e.g. [(START_LOC, {Fleets Req}, {possible dest}), ...]
- - convoy_paths_dest - Contains a dictionary of possible paths to reach destination from start or None
- e.g. {start_loc: {dest_loc_1: [{fleets}, {fleets}, {fleets}], dest_loc_2: [{fleets, fleets}]}
- - daide_port: for client games only. Port when a DAIDE bot can connect, to play with this game.
- - deadline: integer: game deadline in seconds.
- - dislodged - Contains a dictionary of dislodged units (and the site that dislodged them')
- e.g. { 'A PAR': 'MAR' }
- - error - Contains a list of errors that the game generated
- e.g. ['NO MASTER SPECIFIED']
- - fixed_state - used when game is a context of a with-block.
- Store values that define the game state when entered in with-statement.
- Compared to actual fixed state to detect any changes in methods where changes are not allowed.
- Reset to None when exited from with-statement.
- - game_id: String that contains the current game's ID
- e.g. '123456'
- - lost - Contains a dictionary of centers that have been lost during the term
- e.g. {'PAR': 'FRANCE'} to indicate that PAR was lost by France (previous owner)
- - map: Contains a reference to the current map (Map instance)
- e.g. map = Map('standard')
- - map_name: Contains a reference to the name of the map that was loaded (or a full path to a custom map file)
- e.g. map_name = 'standard' or map_name = '/some/path/to/file.map'
- - messages (only for non-observer games): history of messages exchanged inside this game.
- Sorted dict mapping message timestamps to message objects (instances of diplomacy.Message).
- Format: {message.time_sent => message}
- - message_history: history of messages through all played phases.
- Sorted dict mapping a short phase name to a message dict
- (with same format as field `message` describe above).
- Format: {short phase name => {message.time_sent => message}}
- Wrapped in a sorted dict at runtime, see method __init__().
- - meta_rules - Contains the rules that have been processed as directives
- e.g. ['NO_PRESS']
- - n_controls: integer: exact number of controlled powers allowed for this game.
- If game start mode is not START_MASTER, then game starts as soon as this number of powers
- are controlled.
- - no_rules - Contains the list of rules that have been disabled (prefixed with '!')
- e.g ['NO_PRESS']
- - note - A note to display on the rendering
- e.g. 'Winner: FRANCE'
- - observer_level: for client games only. Highest observation level allowed for associated client user.
- Either "master_type", "omniscient_type" or "observer_type".
- - orders - Contains the list of current orders (not yet processed)
- e.g. {'A PAR': '- A MAR'}
- - ordered_units - Contains a dictionary of the units ordered by each power in the last phase
- e.g. {'FRANCE': ['A PAR', 'A MAR'], 'ENGLAND': ... }
- - order_history - Contains the history of orders from each player from the beginning of the game.
- Sorted dict mapping a short phase name to a dictionary of orders
- (powers names as keys, powers orders as values).
- Format: {short phase name => {power name => [orders]}}
- Wrapped in a sorted dict at runtime, see method __init__().
- - outcome - Contains the game outcome
- e.g. [lastPhase, victor1, victor2, victor3]
- - phase: String that contains a long representation of the current phase
- e.g. 'SPRING 1901 MOVEMENT'
- - phase_type: Indicates the current phase type
- (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjustment, '-' for non-playing phase)
- e.g. 'M'
- - popped - Contains a list of all retreaters who didn't make it
- e.g. ['A PAR', 'A MAR']
- - powers - Contains a dictionary mapping power names to power instances in the game
- e.g. {'FRANCE': FrancePower, 'ENGLAND': EnglishPower, ...}
- - registration_password: ** hashed ** version of password to be sent by a player to join this game.
- - renderer - Contains the object in charge of rendering the map
- e.g. Renderer()
- - result - Contains the result of the action for each unit.
- In Movement Phase, result can be 'no convoy', 'bounce', 'void', 'cut', 'dislodged', 'disrupted'
- e.g. { 'A PAR': ['cut', 'void'] }
- In Retreats phase, result can be 'bounce', 'disband', 'void'
- e.g. { 'A PAR': ['cut', 'void'] }
- In Adjustments phase, result can be 'void' or ''
- e.g. { 'A PAR': ['', 'void'] } # e.g. to indicate a successful build, and a void build.
- - result_history - Contains the history of orders results for all played phases.
- Sorted dict mapping a short phase name to a dictionary of order results for this phase.
- Dictionary of order results maps a unit to a list of results. See field result for more details.
- Format: {short phase name => {unit => [results]}}
- Wrapped in a sorted dict at runtime, see method __init__().
- - role: game type (observer, omniscient, player or server game).
- Either a power name (for player game) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES.
- - rules: Contains a list of active rules
- e.g. ['NO_PRESS', ...]
- - state_history: history of previous game states (returned by method get_state()) for this game.
- Sorted dict mapping a short phase name to a game state.
- Each game state is associated to a timestamp generated when state is created by method get_state().
- State timestamp then represents the "end" time of the state, ie. time when this state was saved and
- archived in state history.
- Format: {short phase name => state}
- Wrapped in a sorted dict at runtime, see method __init__().
- - status: game status (forming, active, paused, completed or canceled).
- Possible values in diplomacy.utils.strings.ALL_GAME_STATUSES.
- - supports - Contains a dictionary of support for each unit
- - Format: { 'unit': [nb_of_support, [list of supporting units]] }
- e.g. { 'A PAR': [2, ['A MAR']] }
- 2 support, but the Marseille support does NOT count toward dislodgment
- - timestamp_created: timestamp in microseconds when game object was created on server side.
- - victory - Indicates the number of SUPPLY [default] centers one power must control to win the game
- - Format: [reqFirstYear, reqSecondYear, ..., reqAllFurtherYears]
- e.g. [10,10,18] for 10 the 1st year, 10 the 2nd year, 18 year 3+
- - win - Indicates the minimum number of centers required to win
- e.g. 3
- - zobrist_hash - Contains the zobrist hash representing the current state of this game
- e.g. 12545212418541325
-
- ----- Caches ----
- - _unit_owner_cache - Contains a dictionary with (unit, coast_required) as key and owner as value
- - Set to Note when the cache is not built
- e.g. {('A PAR', True): <FRANCE>, ('A PAR', False): <FRANCE>), ...}
+ """ Game class.
+
+ Properties:
+
+ - **combat**:
+
+ - Dictionary of dictionaries containing the strength of every attack on a location (including units
+ who don't count toward dislodgment)
+ - Format: {loc: attack_strength: [ ['src loc', [support loc] ]}
+ - e.g. ``{ 'MUN': { 1 : [ ['A MUN', [] ], ['A RUH', [] ] ], 2 : [ ['A SIL', ['A BOH'] ] ] } }``.
+ MUN is holding, being attack without support from RUH and being attacked with support from SIL (S from BOH)
+
+ - **command**: contains the list of finalized orders to be processed
+ (same format as orders, but without .order). e.g. {'A PAR': '- A MAR'}
+ - **controlled_powers**: *(for client games only)*. List of powers currently controlled
+ by associated client user.
+ - **convoy_paths**:
+
+ - Contains the list of remaining convoys path for each convoyed unit to reach their destination
+ - Note: This is used to see if there are still active convoy paths remaining.
+ - Note: This also include the start and ending location
+ - e.g. {'A PAR': [ ['PAR', 'ION','NAO', 'MAR], ['PAR', 'ION', 'MAR'] ], ... }
+
+ - **convoy_paths_possible**:
+
+ - Contains the list of possible convoy paths given the current fleet locations or None
+ - e.g. [(START_LOC, {Fleets Req}, {possible dest}), ...]
+
+ - **convoy_paths_dest**:
+
+ - Contains a dictionary of possible paths to reach destination from start or None
+ - e.g. {start_loc: {dest_loc_1: [{fleets}, {fleets}, {fleets}], dest_loc_2: [{fleets, fleets}]}
+
+ - **daide_port**: *(for client games only)*. Port when a DAIDE bot can connect, to play with this game.
+ - **deadline**: integer: game deadline in seconds.
+ - **dislodged**: contains a dictionary of dislodged units (and the site that dislodged them').
+ e.g. { 'A PAR': 'MAR' }
+ - **error**: contains a list of errors that the game generated. e.g. ['NO MASTER SPECIFIED']
+ - **fixed_state**:
+
+ - used when game is a context of a with-block.
+ - Store values that define the game state when entered in with-statement.
+ - Compared to actual fixed state to detect any changes in methods where changes are not allowed.
+ - Reset to None when exited from with-statement.
+
+ - **game_id**: String that contains the current game's ID. e.g. '123456'
+ - **lost**:
+
+ - Contains a dictionary of centers that have been lost during the term
+ - e.g. {'PAR': 'FRANCE'} to indicate that PAR was lost by France (previous owner)
+
+ - **map**: Contains a reference to the current map (Map instance). e.g. map = Map('standard')
+ - **map_name**: Contains a reference to the name of the map that was loaded (or a path to a custom map file)
+ e.g. map_name = 'standard' or map_name = '/some/path/to/file.map'
+ - **messages** *(for non-observer games only)*:
+
+ - history of messages exchanged inside this game.
+ - Sorted dict mapping message timestamps to message objects (instances of diplomacy.Message).
+ - Format: {message.time_sent => message}
+
+ - **message_history**:
+
+ - history of messages through all played phases.
+ - Sorted dict mapping a short phase name to a message dict
+ (with same format as field `message` describe above).
+ - Format: {short phase name => {message.time_sent => message}}
+ - Wrapped in a sorted dict at runtime, see method __init__().
+
+ - **meta_rules**: contains the rules that have been processed as directives. e.g. ['NO_PRESS']
+ - **n_controls**: integer:
+
+ - exact number of controlled powers allowed for this game.
+ - If game start mode is not START_MASTER, then game starts as soon as
+ this number of powers are controlled.
+
+ - **no_rules**: contains the list of rules that have been disabled (prefixed with '!').
+ e.g ['NO_PRESS']
+ - **note**: a note to display on the rendering. e.g. 'Winner: FRANCE'
+ - **observer_level** *(for client games only)*:
+
+ - Highest observation level allowed for associated client user.
+ - Either "master_type", "omniscient_type" or "observer_type".
+
+ - **orders**: contains the list of current orders (not yet processed). e.g. {'A PAR': '- A MAR'}
+ - **ordered_units**:
+
+ - Contains a dictionary of the units ordered by each power in the last phase
+ - e.g. {'FRANCE': ['A PAR', 'A MAR'], 'ENGLAND': ... }
+
+ - **order_history**:
+
+ - Contains the history of orders from each player from the beginning of the game.
+ - Sorted dict mapping a short phase name to a dictionary of orders
+ (powers names as keys, powers orders as values).
+ - Format: {short phase name => {power name => [orders]}}
+ - Wrapped in a sorted dict at runtime, see method __init__().
+
+ - **outcome**: contains the game outcome. e.g. [lastPhase, victor1, victor2, victor3]
+ - **phase**: string that contains a long representation of the current phase.
+ e.g. 'SPRING 1901 MOVEMENT'
+ - **phase_type**: indicates the current phase type.
+ e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjustment, '-' for non-playing phase
+ - **popped**: contains a list of all retreaters who didn't make it. e.g. ['A PAR', 'A MAR']
+ - **powers**:
+
+ - Contains a dictionary mapping power names to power instances in the game
+ - e.g. {'FRANCE': FrancePower, 'ENGLAND': EnglishPower, ...}
+
+ - **registration_password**: ** hashed ** version of password to be sent by a player to join this game.
+ - **renderer**: contains the object in charge of rendering the map. e.g. Renderer()
+ - **result**:
+
+ - Contains the result of the action for each unit.
+ - In Movement Phase, result can be 'no convoy', 'bounce', 'void', 'cut', 'dislodged', 'disrupted'.
+ e.g. { 'A PAR': ['cut', 'void'] }
+ - In Retreats phase, result can be 'bounce', 'disband', 'void'.
+ e.g. { 'A PAR': ['cut', 'void'] }
+ - In Adjustments phase, result can be 'void' or ''.
+ e.g. { 'A PAR': ['', 'void'] } # e.g. to indicate a successful build, and a void build.
+
+ - **result_history**:
+
+ - Contains the history of orders results for all played phases.
+ - Sorted dict mapping a short phase name to a dictionary of order results for this phase.
+ - Dictionary of order results maps a unit to a list of results. See field result for more details.
+ - Format: {short phase name => {unit => [results]}}
+ - Wrapped in a sorted dict at runtime, see method __init__().
+
+ - **role**: Either a power name (for player game) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES.
+ - **rules**: Contains a list of active rules. e.g. ['NO_PRESS', ...]. Default is
+ :const:`diplomacy.utils.constants.DEFAULT_GAME_RULES`.
+ - **state_history**:
+
+ - history of previous game states (returned by method get_state()) for this game.
+ - Sorted dict mapping a short phase name to a game state.
+ - Each game state is associated to a timestamp generated
+ when state is created by method get_state().
+ - State timestamp then represents the "end" time of the state,
+ ie. time when this state was saved and archived in state history.
+ - Format: {short phase name => state}
+ - Wrapped in a sorted dict at runtime, see method __init__().
+
+ - **status**: game status (forming, active, paused, completed or canceled).
+ Possible values in diplomacy.utils.strings.ALL_GAME_STATUSES.
+ - **supports**:
+
+ - Contains a dictionary of support for each unit
+ - Format: { 'unit': [nb_of_support, [list of supporting units]] }
+ - e.g. { 'A PAR': [2, ['A MAR']] }. 2 support, but the Marseille support
+ does NOT count toward dislodgment
+
+ - **timestamp_created**: timestamp in microseconds when game object was created on server side.
+ - **victory**:
+
+ - Indicates the number of SUPPLY [default] centers one power must control to win the game
+ - Format: [reqFirstYear, reqSecondYear, ..., reqAllFurtherYears]
+ - e.g. [10,10,18] for 10 the 1st year, 10 the 2nd year, 18 year 3+
+
+ - **win** - Indicates the minimum number of centers required to win. e.g. 3
+ - **zobrist_hash** - Contains the zobrist hash representing the current state of this game.
+ e.g. 12545212418541325
+
+ Cache properties:
+
+ - **unit_owner_cache**:
+
+ - Contains a dictionary with (unit, coast_required) as key and owner as value
+ - Set to Note when the cache is not built
+ - e.g. {('A PAR', True): <FRANCE>, ('A PAR', False): <FRANCE>), ...}
+
"""
# pylint: disable=too-many-instance-attributes
__slots__ = ['victory', 'no_rules', 'meta_rules', 'phase', 'note', 'map', 'powers', 'outcome', 'error', 'popped',
@@ -262,15 +313,19 @@ class Game(Jsonable):
raise exceptions.NaturalIntegerException('n_controls must be a natural integer.')
if self.deadline < 0:
raise exceptions.NaturalIntegerException('Deadline must be a natural integer.')
+
# Check rules.
if rules is None:
- rules = ['SOLITAIRE', 'NO_PRESS', 'IGNORE_ERRORS', 'POWER_CHOICE']
+ rules = list(DEFAULT_GAME_RULES)
+
# Set game rules.
for rule in rules:
self.add_rule(rule)
+
# Check settings about rule NO_DEADLINE.
if 'NO_DEADLINE' in self.rules:
self.deadline = 0
+
# Check settings about rule SOLITAIRE.
if 'SOLITAIRE' in self.rules:
self.n_controls = 0
@@ -375,8 +430,9 @@ class Game(Jsonable):
@property
def power(self):
""" (only for player games) Return client power associated to this game.
+
:return: a Power object.
- :rtype: Power
+ :rtype: diplomacy.engine.power.Power
"""
return self.powers[self.role] if self.is_player_game() else None
@@ -408,18 +464,19 @@ class Game(Jsonable):
def current_state(self):
""" Returns the game object. To be used with the following syntax:
- ```
- with game.current_state():
- orders = players.get_orders(game, power_name)
- game.set_orders(power_name, orders)
- ```
+
+ .. code-block:: python
+
+ with game.current_state():
+ orders = players.get_orders(game, power_name)
+ game.set_orders(power_name, orders)
"""
return self
def __enter__(self):
""" Enter into game context. Initialize fixed state.
- Raise an exception if fixed state is already initialized to a different state, to prevent using the game
- into multiple contexts at same time.
+ Raise an exception if fixed state is already initialized to a different state,
+ to prevent using the game into multiple contexts at same time.
"""
current_state = (self.get_current_phase(), self.get_hash())
if self.fixed_state and self.fixed_state != current_state:
@@ -433,6 +490,7 @@ class Game(Jsonable):
def is_fixed_state_unchanged(self, log_error=True):
""" Check if actual state matches saved fixed state, if game is used as context of a with-block.
+
:param log_error: Boolean that indicates to log an error if state has changed
:return: boolean that indicates if the state has changed.
"""
@@ -468,7 +526,12 @@ class Game(Jsonable):
return common.is_valid_password(registration_password, self.registration_password)
def is_controlled(self, power_name):
- """ Return True if given power name is currently controlled. """
+ """ Return True if given power name is currently controlled.
+
+ :param power_name: power name
+ :type power_name: str
+ :rtype: bool
+ """
return self.get_power(power_name).is_controlled()
def is_dummy(self, power_name):
@@ -512,13 +575,10 @@ class Game(Jsonable):
expected_count = len(self.powers)
return expected_count
- def get_map_power_names(self):
- """ Return sequence of map power names. """
- return self.powers.keys()
-
def get_dummy_power_names(self):
- """ Return sequence of dummy power names. """
- return set(power_name for power_name in self.get_map_power_names() if self.is_dummy(power_name))
+ """ Return sequence of not eliminated dummy power names. """
+ return set(power_name for power_name in self.get_map_power_names()
+ if self.is_dummy(power_name) and not self.get_power(power_name).is_eliminated())
def get_dummy_unordered_power_names(self):
""" Return a sequence of playable dummy power names
@@ -555,7 +615,9 @@ class Game(Jsonable):
return playable_power_names[random.randint(0, len(playable_power_names) - 1)]
def get_latest_timestamp(self):
- """ Return timestamp of latest data saved into this game (either current state, archived state or message).
+ """ Return timestamp of latest data saved into this game
+ (either current state, archived state or message).
+
:return: a timestamp
:rtype: int
"""
@@ -570,6 +632,7 @@ class Game(Jsonable):
def filter_messages(cls, messages, game_role, timestamp_from=None, timestamp_to=None):
""" Filter given messages based on given game role between given timestamps (bounds included).
See method diplomacy.utils.SortedDict.sub() about bound rules.
+
:param messages: a sorted dictionary of messages to filter.
:param game_role: game role requiring messages. Either a special power name
(PowerName.OBSERVER or PowerName.OMNISCIENT), a power name, or a list of power names.
@@ -603,16 +666,23 @@ class Game(Jsonable):
def get_phase_history(self, from_phase=None, to_phase=None, game_role=None):
""" Return a list of game phase data from game history between given phases (bounds included).
Each GamePhaseData object contains game state, messages, orders and order results for a phase.
+
:param from_phase: either:
+
- a string: phase name
- an integer: index of phase in game history
- None (default): lowest phase stored in game history
+
:param to_phase: either:
+
- a string: phase name
- an integer: index of phase in game history
- None (default): latest phase stored in game history
- :param game_role (optional): role of game for which phase history is retrieved.
+
+ :param game_role: (optional) role of game for which phase history is retrieved.
If none, messages in game history will not be filtered.
+
+ :return: a list of GamePhaseHistory objects
"""
if isinstance(from_phase, int):
from_phase = self.state_history.key_from_index(from_phase)
@@ -653,6 +723,7 @@ class Game(Jsonable):
def extend_phase_history(self, game_phase_data):
""" Add data from a game phase to game history.
+
:param game_phase_data: a GamePhaseData object.
:type game_phase_data: GamePhaseData
"""
@@ -673,10 +744,11 @@ class Game(Jsonable):
def draw(self, winners=None):
""" Force a draw for this game, set status as COMPLETED and finish the game.
- :param winners: (optional) either None (all powers remaining to map are considered winners) or a sequence
- of required power names to be considered as winners.
- :return: a couple (previous state, current state) with game state before the draw and game state after
- the draw.
+
+ :param winners: (optional) either None (all powers remaining to map are considered winners)
+ or a sequence of required power names to be considered as winners.
+ :return: a couple (previous state, current state)
+ with game state before the draw and game state after the draw.
"""
if winners is None:
# Draw with all powers which still have units in map.
@@ -726,6 +798,7 @@ class Game(Jsonable):
def update_dummy_powers(self, dummy_power_names):
""" Force all power associated to given dummy power names to be uncontrolled.
+
:param dummy_power_names: Sequence of required dummy power names.
"""
for dummy_power_name in dummy_power_names:
@@ -734,6 +807,7 @@ class Game(Jsonable):
def update_powers_controllers(self, powers_controllers, timestamps):
""" Update powers controllers.
+
:param powers_controllers: a dictionary mapping a power name to a controller name.
:param timestamps: a dictionary mapping a power name to timestamp when related controller
(in powers_controllers) was associated to power.
@@ -746,6 +820,7 @@ class Game(Jsonable):
""" Create a undated (without timestamp) power message to be sent from a power to another via server.
Server will answer with timestamp, and message will be updated
and added to local game messages.
+
:param recipient: recipient power name (string).
:param body: message body (string).
:return: a new GameMessage object.
@@ -759,6 +834,7 @@ class Game(Jsonable):
def new_global_message(self, body):
""" Create an undated (without timestamp) global message to be sent from a power via server.
Server will answer with timestamp, and message will be updated and added to local game messages.
+
:param body: message body (string).
:return: a new GameMessage object.
:rtype: Message
@@ -770,6 +846,7 @@ class Game(Jsonable):
""" Add message to current game data.
Only a server game can add a message with no timestamp:
game will auto-generate a timestamp for the message.
+
:param message: a GameMessage object to add.
:return: message timestamp.
:rtype: int
@@ -820,13 +897,24 @@ class Game(Jsonable):
# ==============
# Basic methods.
# ==============
+
+ def get_map_power_names(self):
+ """ Return sequence of map power names. """
+ return self.powers.keys()
+
+ def get_current_phase(self):
+ """ Returns the current phase (format 'S1901M' or 'FORMING' or 'COMPLETED') """
+ return self._phase_abbr()
+
def get_units(self, power_name=None):
""" Retrieves the list of units for a power or for all powers
- :param power_name: Optional. The name of the power (e.g. 'FRANCE') or None for all powers
- :return: A list of units (e.g. ['A PAR', 'A MAR']) if a power name is provided
- or a dictionary of powers with their units if None is provided (e.g. {'FRANCE': [...], ...}
- Note: Dislodged units will appear with a leading asterisk (e.g. '*A PAR')
+ :param power_name: Optional. The name of the power (e.g. ``'FRANCE'``) or None for all powers
+ :return: A list of units (e.g. ``['A PAR', 'A MAR']``) if a power name is provided
+ or a dictionary of powers with their units if None is provided
+ (e.g. ``{'FRANCE': [...], ...}``)
+
+ Note: Dislodged units will appear with a leading asterisk (e.g. ``'*A PAR'``)
"""
if power_name is not None:
power_name = power_name.upper()
@@ -842,10 +930,11 @@ class Game(Jsonable):
def get_centers(self, power_name=None):
""" Retrieves the list of owned supply centers for a power or for all powers
+
:param power_name: Optional. The name of the power (e.g. 'FRANCE') or None for all powers
:return: A list of supply centers (e.g. ['PAR', 'MAR']) if a power name is provided
- or a dictionary of powers with their supply centers if None is provided
- (e.g. {'FRANCE': [...], ...}
+ or a dictionary of powers with their supply centers if None is provided
+ (e.g. {'FRANCE': [...], ...}
"""
if power_name is not None:
power_name = power_name.upper()
@@ -861,10 +950,11 @@ class Game(Jsonable):
def get_orders(self, power_name=None):
""" Retrieves the orders submitted by a specific power, or by all powers
+
:param power_name: Optional. The name of the power (e.g. 'FRANCE') or None for all powers
:return: A list of orders (e.g. ['A PAR H', 'A MAR - BUR']) if a power name is provided
- or a dictionary of powers with their orders if None is provided
- (e.g. {'FRANCE': ['A PAR H', 'A MAR - BUR', ...], ...}
+ or a dictionary of powers with their orders if None is provided
+ (e.g. {'FRANCE': ['A PAR H', 'A MAR - BUR', ...], ...}
"""
if power_name is not None:
power_name = power_name.upper()
@@ -893,10 +983,11 @@ class Game(Jsonable):
def get_orderable_locations(self, power_name=None):
""" Find the location requiring an order for a power (or for all powers)
+
:param power_name: Optionally, the name of the power (e.g. 'FRANCE') or None for all powers
:return: A list of orderable locations (e.g. ['PAR', 'MAR']) if a power name is provided
- or a dictionary of powers with their orderable locations if None is not provided
- (e.g. {'FRANCE': [...], ...}
+ or a dictionary of powers with their orderable locations if None is not provided
+ (e.g. {'FRANCE': [...], ...}
"""
if power_name is not None:
power_name = power_name.upper()
@@ -939,17 +1030,22 @@ class Game(Jsonable):
def get_order_status(self, power_name=None, unit=None, loc=None):
""" Returns a list or a dict representing the order status ('', 'no convoy', 'bounce', 'void', 'cut',
'dislodged', 'disrupted') for orders submitted in the last phase
+
:param power_name: Optional. If provided (e.g. 'FRANCE') will only return the order status of that
- power's orders
+ power's orders
:param unit: Optional. If provided (e.g. 'A PAR') will only return that specific unit order status.
:param loc: Optional. If provided (e.g. 'PAR') will only return that specific loc order status.
Mutually exclusive with unit
:param phase_type: Optional. Returns the results of a specific phase type (e.g. 'M', 'R', or 'A')
- :return: If unit is provided a list (e.g. [] or ['void', 'dislodged'])
- If loc is provided, a couple of unit and list (e.g. ('A PAR', ['void', 'dislodged'])),
- or loc, [] if unit not found.
- If power is provided a dict (e.g. {'A PAR': ['void'], 'A MAR': []})
- Otherwise a 2-level dict (e.g. {'FRANCE: {'A PAR': ['void'], 'A MAR': []}, 'ENGLAND': {}, ... }
+ :return:
+
+ - If unit is provided a list (e.g. [] or ['void', 'dislodged'])
+ - If loc is provided, a couple of unit and list (e.g. ('A PAR', ['void', 'dislodged'])),
+ or (loc, []) if unit not found.
+ - If power is provided a dict (e.g. {'A PAR': ['void'], 'A MAR': []})
+ - Otherwise a 2-level dict
+ (e.g. {'FRANCE: {'A PAR': ['void'], 'A MAR': []}, 'ENGLAND': {}, ... }
+
"""
# Specific location
if unit or loc:
@@ -986,15 +1082,17 @@ class Game(Jsonable):
def get_power(self, power_name):
""" Retrieves a power instance from given power name.
+
:param power_name: name of power instance to retrieve. Power name must be as given
in map file.
:return: the power instance, or None if power name is not found.
- :rtype: Power
+ :rtype: diplomacy.engine.power.Power
"""
return self.powers.get(power_name, None)
def set_units(self, power_name, units, reset=False):
""" Sets units directly on the map
+
:param power_name: The name of the power who will own the units (e.g. 'FRANCE')
:param units: An unit (e.g. 'A PAR') or a list of units (e.g. ['A PAR', 'A MAR']) to set
Note units starting with a '*' will be set as dislodged
@@ -1078,6 +1176,7 @@ class Game(Jsonable):
def set_centers(self, power_name, centers, reset=False):
""" Transfers supply centers ownership
+
:param power_name: The name of the power who will control the supply centers (e.g. 'FRANCE')
:param centers: A loc (e.g. 'PAR') or a list of locations (e.g. ['PAR', 'MAR']) to transfer
:param reset: Boolean. If, removes ownership of all power's SC before transferring ownership of the new SC
@@ -1113,16 +1212,18 @@ class Game(Jsonable):
def set_orders(self, power_name, orders, expand=True, replace=True):
""" Sets the current orders for a power
+
:param power_name: The name of the power (e.g. 'FRANCE')
:param orders: The list of orders (e.g. ['A MAR - PAR', 'A PAR - BER', ...])
:param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.)
- If false, expect orders in the following format. False gives a performance improvement.
+ If false, expect orders in the following format. False gives a performance improvement.
:param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders.
:return: Nothing
- Expected format:
- A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI
- A IRO R MAO, A IRO D, A LON B, F LIV B
+ Expected format: ::
+
+ A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI,
+ F NWG C A NWY - EDI, A IRO R MAO, A IRO D, A LON B, F LIV B
"""
if not self.is_fixed_state_unchanged(log_error=bool(orders)):
return
@@ -1155,6 +1256,7 @@ class Game(Jsonable):
def set_wait(self, power_name, wait):
""" Set wait flag for a power.
+
:param power_name: name of power to set wait flag.
:param wait: wait flag (boolean).
"""
@@ -1165,13 +1267,14 @@ class Game(Jsonable):
if not self.has_power(power_name):
return
- power = self.get_power(power_name.upper()) # type: Power
+ power = self.get_power(power_name.upper()) # type: diplomacy.engine.power.Power
power.wait = wait
def clear_units(self, power_name=None):
""" Clear the power's units
+
:param power_name: Optional. The name of the power whose units will be cleared (e.g. 'FRANCE'),
- otherwise all units on the map will be cleared
+ otherwise all units on the map will be cleared
:return: Nothing
"""
for power in self.powers.values():
@@ -1181,8 +1284,9 @@ class Game(Jsonable):
def clear_centers(self, power_name=None):
""" Removes ownership of supply centers
+
:param power_name: Optional. The name of the power whose centers will be cleared (e.g. 'FRANCE'),
- otherwise all centers on the map will lose ownership.
+ otherwise all centers on the map will lose ownership.
:return: Nothing
"""
for power in self.powers.values():
@@ -1192,8 +1296,9 @@ class Game(Jsonable):
def clear_orders(self, power_name=None):
""" Clears the power's orders
+
:param power_name: Optional. The name of the power to clear (e.g. 'FRANCE') or will clear orders for
- all powers if None.
+ all powers if None.
:return: Nothing
"""
if not self.is_fixed_state_unchanged():
@@ -1210,10 +1315,6 @@ class Game(Jsonable):
self.convoy_paths_possible, self.convoy_paths_dest = None, None
self._unit_owner_cache = None
- def get_current_phase(self):
- """ Returns the current phase (format 'S1901M' or 'FORMING' or 'COMPLETED' """
- return self._phase_abbr()
-
def set_current_phase(self, new_phase):
""" Changes the phase to the specified new phase (e.g. 'S1901M') """
if new_phase in ('FORMING', 'COMPLETED'):
@@ -1225,9 +1326,10 @@ class Game(Jsonable):
def render(self, incl_orders=True, incl_abbrev=False, output_format='svg'):
""" Renders the current game and returns its image representation
+
:param incl_orders: Optional. Flag to indicate we also want to render orders.
:param incl_abbrev: Optional. Flag to indicate we also want to display the provinces abbreviations.
- :param output_format: The desired output format.
+ :param output_format: The desired output format. Currently, only 'svg' is supported.
:return: The rendered image in the specified format.
"""
if not self.renderer:
@@ -1236,6 +1338,7 @@ class Game(Jsonable):
def add_rule(self, rule):
""" Adds a rule to the current rule list
+
:param rule: Name of rule to add (e.g. 'NO_PRESS')
:return: Nothing
"""
@@ -1274,6 +1377,7 @@ class Game(Jsonable):
def remove_rule(self, rule):
""" Removes a rule from the current rule list
+
:param rule: Name of rule to remove (e.g. 'NO_PRESS')
:return: Nothing
"""
@@ -1282,6 +1386,7 @@ class Game(Jsonable):
def load_map(self, reinit_powers=True):
""" Load a map and process directives
+
:param reinit_powers: Boolean. If true, empty powers dict.
:return: Nothing, but stores the map in self.map
"""
@@ -1314,6 +1419,7 @@ class Game(Jsonable):
def process(self):
""" Processes the current phase of the game.
+
:return: game phase data with data before processing.
"""
previous_phase = self._phase_wrapper_type(self.current_short_phase)
@@ -1361,6 +1467,7 @@ class Game(Jsonable):
def rebuild_hash(self):
""" Completely recalculate the Zobrist hash
+
:return: The updated hash value
"""
self.zobrist_hash = 0
@@ -1391,13 +1498,14 @@ class Game(Jsonable):
def update_hash(self, power, unit_type='', loc='', is_dislodged=False, is_center=False, is_home=False):
""" Updates the zobrist hash for the current game
- :param power: The name of the power owning the unit, supply center or home
- :param unit_type: Contains the unit type of the unit being added or remove from the board ('A' or 'F')
- :param loc: Contains the location of the unit, supply center, of home being added or remove
- :param is_dislodged: Indicates that the unit being added/removed is dislodged
- :param is_center: Indicates that the location being added/removed is a supply center
- :param is_home: Indicates that the location being added/removed is a home
- :return: Nothing
+
+ :param power: The name of the power owning the unit, supply center or home
+ :param unit_type: Contains the unit type of the unit being added or remove from the board ('A' or 'F')
+ :param loc: Contains the location of the unit, supply center, of home being added or remove
+ :param is_dislodged: Indicates that the unit being added/removed is dislodged
+ :param is_center: Indicates that the location being added/removed is a supply center
+ :param is_home: Indicates that the location being added/removed is a home
+ :return: Nothing
"""
if self.map is None:
return
@@ -1442,6 +1550,7 @@ class Game(Jsonable):
def set_phase_data(self, phase_data, clear_history=True):
""" Set game from phase data.
+
:param phase_data: either a GamePhaseData or a list of GamePhaseData.
If phase_data is a GamePhaseData, it will be treated as a list of GamePhaseData with 1 element.
Last phase data in given list will be used to set current game internal state.
@@ -1485,8 +1594,8 @@ class Game(Jsonable):
See field order_history to get orders from previous phases.
To get a complete state of all data in this game object, consider using method Game.to_dict().
- :param make_copy: Boolean. If true, a deep copy of the game state is returned, otherwise the attributes are
- returned directly.
+ :param make_copy: Boolean. If true, a deep copy of the game state is returned,
+ otherwise the attributes are returned directly.
:return: The internal saved state (dict) of the game
"""
state = {}
@@ -1527,6 +1636,7 @@ class Game(Jsonable):
def set_state(self, state, clear_history=True):
""" Sets the game from a saved internal state
+
:param state: The saved state (dict)
:param clear_history: Boolean. If true, all game histories are cleared.
:return: Nothing
@@ -1576,6 +1686,7 @@ class Game(Jsonable):
def get_all_possible_orders(self):
""" Computes a list of all possible orders for all locations
+
:return: A dictionary with locations as keys, and their respective list of possible orders as values
"""
# pylint: disable=too-many-branches,too-many-nested-blocks
@@ -1806,6 +1917,7 @@ class Game(Jsonable):
def _is_convoyer(self, army, loc):
""" Detects if there is a convoyer at thru location for army/fleet (e.g. can an army be convoyed through PAR)
+
:param army: Boolean to indicate if unit being convoyed is army (1) or fleet (0)
:param loc: Location we are checking (e.g. 'STP/SC')
:return: Boolean to indicate if unit can be convoyed through location
@@ -1824,6 +1936,7 @@ class Game(Jsonable):
def _is_moving_via_convoy(self, unit):
""" Determines if a unit is moving via a convoy or through land
+
:param unit: The name of the unit (e.g. 'A PAR')
:return: A boolean (True, False) to indicate if the unit is moving via convoy
"""
@@ -1838,6 +1951,7 @@ class Game(Jsonable):
def _has_convoy_path(self, unit, start, end, convoying_loc=None):
""" Determines if there is a convoy path for unit
+
:param unit: The unit BEING convoyed (e.g. 'A' or 'F')
:param start: The start location of the unit (e.g. 'LON')
:param end: The destination of the unit (e.g. 'MAR')
@@ -1854,6 +1968,7 @@ class Game(Jsonable):
def _get_convoying_units_for_path(self, unit, start, end):
""" Returns a list of units who have submitted orders to convoy 'unit' from 'start' to 'end'
+
:param unit: The unit BEING convoyed (e.g. 'A' or 'F')
:param start: The start location of the unit (e.g. 'LON')
:param end: The destination of the unit (e.g. 'MAR')
@@ -1869,10 +1984,11 @@ class Game(Jsonable):
def _get_convoy_destinations(self, unit, start, unit_is_convoyer=False, exclude_convoy_locs=None):
""" Returns a list of possible convoy destinations for a unit
+
:param unit: The unit BEING convoyed (e.g. 'A' or 'F')
:param start: The start location of the unit (e.g. 'LON')
:param unit_is_convoyer: Boolean flag. If true, list all the dests that an unit being convoyed by unit
- could reach
+ could reach
:param exclude_convoy_locs: Optional. A list of convoying location that needs to be excluded from all paths.
:return: A list of convoying destinations (e.g. ['PAR', 'MAR']) that can be reached from start
"""
@@ -1908,11 +2024,12 @@ class Game(Jsonable):
def _get_convoy_paths(self, unit_type, start, end, via, convoying_units):
""" Return a list of all possible convoy paths (using convoying units) from start to end
+
:param unit_type: The unit type BEING convoyed (e.g. 'A' or 'F')
:param start: The start location of the unit (e.g. 'LON')
:param end: The destination of the unit (e.g. 'MAR')
:param via: Boolean flag (0 or 1) to indicate if we want only paths with a local convoyer, or also paths
- including only foreign convoyers
+ including only foreign convoyers
:param convoying_units: The list of units who can convoy the unit
:return: A list of paths from start to end using convoying_units
"""
@@ -1956,6 +2073,7 @@ class Game(Jsonable):
def _get_distance_to_home(self, unit_type, start, homes):
""" Calculate the distance from unit to one of its homes
Armies can move over water (4.D.8 choice d)
+
:param unit_type: The unit type to calculate distance (e.g. 'A' or 'F')
:param start: The start location of the unit (e.g. 'LON')
:param homes: The list of homes (first one reached calculates the distance)
@@ -2000,15 +2118,17 @@ class Game(Jsonable):
# ====================================================================
def _valid_order(self, power, unit, order, report=1):
""" Determines if an order is valid
+
:param power: The power submitting the order
:param unit: The unit being affected by the order (e.g. 'A PAR')
:param order: The actual order (e.g. 'H' or 'S A MAR')
:param report: Boolean to report errors in self.errors
:return: One of the following:
- None - The order is NOT valid at all
- -1 - It is NOT valid, BUT it does not get reported because it may be used to signal support
- 0 - It is valid, BUT some unit mentioned does not exist
- 1 - It is completed valid
+
+ * None - The order is NOT valid at all
+ * -1 - It is NOT valid, BUT it does not get reported because it may be used to signal support
+ * 0 - It is valid, BUT some unit mentioned does not exist
+ * 1 - It is completed valid
"""
# pylint: disable=too-many-return-statements,too-many-branches,too-many-statements
# No order
@@ -2261,8 +2381,9 @@ class Game(Jsonable):
def _expand_order(self, word):
""" Detects errors in order, convert to short version, and expand the default coast if necessary
+
:param word: The words (e.g. order.split()) for an order
- (e.g. ['England:', 'Army', 'Rumania', 'SUPPORT', 'German', 'Army', 'Bulgaria']).
+ (e.g. ['England:', 'Army', 'Rumania', 'SUPPORT', 'German', 'Army', 'Bulgaria']).
:return: The compacted and expanded order (e.g. ['A', 'RUM', 'S', 'A', 'BUL'])
"""
if not word:
@@ -2350,6 +2471,7 @@ class Game(Jsonable):
For Fleets: Adjust to correct coast if wrong coast is specified
For Armies: Removes coast if coast is specified
(e.g. if F is on SPA/SC but the order is F SPA/NC - LYO, the coast will be added or corrected)
+
:param word: A list of tokens (e.g. ['F', 'GRE', '-', 'BUL'])
:return: The updated list of tokens (e.g. ['F', 'GRE', '-', 'BUL/SC'])
"""
@@ -2393,6 +2515,7 @@ class Game(Jsonable):
def _add_unit_types(self, item):
""" Adds any missing "A" and "F" designations and (current) coastal locations for fleets.
+
:param item: The words for expand_order() (e.g. ['A', 'RUM', 'S', 'BUL'])
:return: The list of items with A/F and coasts added (e.g. ['A', 'RUM', 'S', 'A', 'BUL'])
"""
@@ -2431,8 +2554,9 @@ class Game(Jsonable):
return word
def _add_coasts(self):
- """ This method adds the matching coast to orders supporting or (portage) convoying a fleet to
- a multi-coast province.
+ """ This method adds the matching coast to orders supporting or (portage)
+ convoying a fleet to a multi-coast province.
+
:return: Nothing
"""
# converting to unique format
@@ -2528,14 +2652,16 @@ class Game(Jsonable):
# ====================================================================
def _load_rules(self):
""" Loads the list of rules and their forced (+) and denied (!) corresponding rules
- :return: A tuple of dictionaries: rules, forced, and denied
- rules = {'NO_CHECK':
- { 'group': '3 Movement Order',
- 'variant': 'standard',
- '!': ['RULE_1', 'RULE_2'],
- '+': ['RULE_3'] } }
- forced = {'payola': 'RULE_4'}
- denied = {'payola': 'RULE_5'}
+
+ :return: A tuple of dictionaries: rules, forced, and denied ::
+
+ rules = {'NO_CHECK':
+ { 'group': '3 Movement Order',
+ 'variant': 'standard',
+ '!': ['RULE_1', 'RULE_2'],
+ '+': ['RULE_3'] } }
+ forced = {'payola': 'RULE_4'}
+ denied = {'payola': 'RULE_5'}
"""
if self.__class__.rule_cache:
return self.__class__.rule_cache
@@ -2612,6 +2738,7 @@ class Game(Jsonable):
# ====================================================================
def _begin(self):
""" Called to begin the game and move to the start phase
+
:return: Nothing
"""
self._move_to_start_phase()
@@ -2655,6 +2782,7 @@ class Game(Jsonable):
def _advance_phase(self):
""" Advance the game to the next phase (skipping phases with no actions)
+
:return: A list of lines to put in the results
"""
@@ -2689,6 +2817,7 @@ class Game(Jsonable):
def _move_to_start_phase(self):
""" Moves to the map's start phase
+
:return: Nothing, but sets the self.phase and self.phase_type settings
"""
# Retrieve the beginning phase and phase type from the map
@@ -2697,8 +2826,9 @@ class Game(Jsonable):
def _find_next_phase(self, phase_type=None, skip=0):
""" Returns the long name of the phase coming immediately after the current phase
+
:param phase_type: The type of phase we are looking for
- (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.)
+ (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.)
:param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after)
:return: The long name of the next phase (e.g. FALL 1905 MOVEMENT)
"""
@@ -2706,8 +2836,9 @@ class Game(Jsonable):
def _find_previous_phase(self, phase_type=None, skip=0):
""" Returns the long name of the phase coming immediately before the current phase
+
:param phase_type: The type of phase we are looking for
- (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.)
+ (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.)
:param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after)
:return: The long name of the previous phase (e.g. SPRING 1905 MOVEMENT)
"""
@@ -2723,6 +2854,7 @@ class Game(Jsonable):
def _check_phase(self):
""" Checks if we need to process a phase, or if we can skip it if there are no actions
+
:return: Boolean (0 or 1) - 0 if we need to process phase, 1 if we can skip it
"""
# pylint: disable=too-many-return-statements
@@ -2786,6 +2918,7 @@ class Game(Jsonable):
def _build_sites(self, power):
""" Returns a list of sites where power can build units
+
:param power: The power instance to check
:return: A list of build sites
"""
@@ -2805,8 +2938,12 @@ class Game(Jsonable):
def _build_limit(self, power, sites=None):
""" Determines the maximum number of builds a power can do in an adjustment phase
- Note: This function assumes that one unit can be built per build sites + alternative sites
- The actual maximum build limit would be less if units are built on alternative sites.
+
+ Note:
+
+ - This function assumes that one unit can be built per build sites + alternative sites.
+ - The actual maximum build limit would be less if units are built on alternative sites.
+
:param power: The power instance to check
:param sites: The power's build sites (or None to compute them)
:return: An integer representing the maximum number of simultaneous builds
@@ -2820,6 +2957,7 @@ class Game(Jsonable):
def _calculate_victory_score(self):
""" Calculates the score to determine win for each power
+
:return: A dict containing the score for each power (e.g. {'FRANCE': 10, 'ENGLAND': 2})
"""
score = {}
@@ -2831,8 +2969,9 @@ class Game(Jsonable):
def _determine_win(self, last_year):
""" Determine if we have a win.
+
:param last_year: A dict containing the score for each power (e.g. {'FRANCE': 10, 'ENGLAND': 2})
- (from the previous year)
+ (from the previous year)
:return: Nothing
"""
victors, this_year = [], self._calculate_victory_score()
@@ -2863,6 +3002,7 @@ class Game(Jsonable):
def _capture_centers(self):
""" In Adjustment Phase, proceed with the capture of occupied supply centers
+
:return: Nothing
"""
victory_score_prev_year = self._calculate_victory_score()
@@ -2913,6 +3053,7 @@ class Game(Jsonable):
def _transfer_center(self, from_power, to_power, center):
""" Transfers a supply center from a power to another
+
:param from_power: The power instance from whom the supply center is transfered
:param to_power: The power instance to whom the supply center is transferred
:param center: The supply center location (e.g. 'PAR')
@@ -2927,6 +3068,7 @@ class Game(Jsonable):
def _finish(self, victors):
""" Indicates that a game is finished and has been won by 'victors'
+
:param victors: The list of victors (e.g. ['FRANCE', 'GERMANY'])
:return: Nothing
"""
@@ -2942,6 +3084,7 @@ class Game(Jsonable):
def _phase_abbr(self, phase=None):
""" Constructs a 5 character representation (S1901M) from a phase (SPRING 1901 MOVEMENT)
+
:param phase: The full phase (e.g. SPRING 1901 MOVEMENT)
:return: A 5 character representation of the phase
"""
@@ -2954,14 +3097,15 @@ class Game(Jsonable):
""" Adds an order for a power
:param power: The power instance issuing the order
:param word: The order (e.g. ['A', 'PAR', '-', 'MAR'])
- :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.)
- If false, expect orders in the following format. False gives a performance improvement.
+ :param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.).
+ If false, expect orders in the following format. False gives a performance improvement.
:param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders.
:return: Nothing, but adds error to self.error
- Expected format:
- A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI
- A IRO R MAO, A IRO D, A LON B, F LIV B
+ Expected format: ::
+
+ A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI,
+ F NWG C A NWY - EDI, A IRO R MAO, A IRO D, A LON B, F LIV B
"""
if not word:
return None
@@ -3019,15 +3163,17 @@ class Game(Jsonable):
def _update_orders(self, power, orders, expand=True, replace=True):
""" Updates the orders of a power
+
:param power: The power instance (or None if updating multiple instances)
:param orders: The updated list of orders
- e.g. ['A MAR - PAR', 'A PAR - BER', ...]
+ e.g. ['A MAR - PAR', 'A PAR - BER', ...]
:param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.)
- If false, expect orders in the following format. False gives a performance improvement.
+ If false, expect orders in the following format. False gives a performance improvement.
:param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders.
:return: Nothing
- Expected format:
+ Expected format: ::
+
A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI
A IRO R MAO, A IRO D, A LON B, F LIV B
"""
@@ -3081,15 +3227,17 @@ class Game(Jsonable):
def _add_retreat_orders(self, power, orders, expand=True, replace=True):
""" Adds a retreat order (Retreats Phase)
+
:param power: The power instance who is submitting orders (or None if power is in the orders)
:param orders: The list of adjustment orders
- (format can be [Country: order], [Country, order, order], or [order,order])
+ (format can be [Country: order], [Country, order, order], or [order,order])
:param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.)
- If false, expect orders in the following format. False gives a performance improvement.
+ If false, expect orders in the following format. False gives a performance improvement.
:param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders.
:return: Nothing, but adds error to self.error
- Expected format:
+ Expected format: ::
+
A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI
A IRO R MAO, A IRO D, A LON B, F LIV B
"""
@@ -3178,14 +3326,16 @@ class Game(Jsonable):
def _update_retreat_orders(self, power, orders, expand=True, replace=True):
""" Updates order for Retreats phase
+
:param power: The power instance submitting the orders
:param orders: The updated orders
:param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.)
- If false, expect orders in the following format. False gives a performance improvement.
+ If false, expect orders in the following format. False gives a performance improvement.
:param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders.
:return: List of processing errors
- Expected format:
+ Expected format: ::
+
A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI
A IRO R MAO, A IRO D, A LON B, F LIV B
"""
@@ -3194,15 +3344,17 @@ class Game(Jsonable):
def _add_adjust_orders(self, power, orders, expand=True, replace=True):
""" Adds an adjustment order (Adjustment Phase)
+
:param power: The power instance who is submitting orders (or None if power is in the orders)
:param orders: The list of adjustment orders (format can be [Country: order],
- [Country, order, order], or [order,order])
+ [Country, order, order], or [order,order])
:param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.)
- If false, expect orders in the following format. False gives a performance improvement.
+ If false, expect orders in the following format. False gives a performance improvement.
:param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders.
:return: Nothing, but adds error to self.error
- Expected format:
+ Expected format: ::
+
A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI
A IRO R MAO, A IRO D, A LON B, F LIV B
"""
@@ -3344,14 +3496,16 @@ class Game(Jsonable):
def _update_adjust_orders(self, power, orders, expand=True, replace=True):
""" Updates order for Adjustment phase
+
:param power: The power instance submitting the orders
:param orders: The updated orders
:param expand: Boolean. If set, performs order expansion and reformatting (e.g. adding unit type, etc.)
- If false, expect orders in the following format. False gives a performance improvement.
+ If false, expect orders in the following format. False gives a performance improvement.
:param replace: Boolean. If set, replace previous orders on same units, otherwise prevents re-orders.
:return: List of processing errors
- Expected format:
+ Expected format: ::
+
A LON H, F IRI - MAO, A IRI - MAO VIA, A WAL S F LON, A WAL S F MAO - IRI, F NWG C A NWY - EDI
A IRO R MAO, A IRO D, A LON B, F LIV B
"""
@@ -3379,6 +3533,7 @@ class Game(Jsonable):
def _default_orders(self, power):
""" Issues default orders for a power (HOLD)
+
:param power: The power instance
:return: Nothing
"""
@@ -3397,6 +3552,7 @@ class Game(Jsonable):
# ====================================================================
def _abuts(self, unit_type, unit_loc, order_type, other_loc):
""" Determines if a order for unit_type from unit_loc to other_loc is adjacent (Support and convoy only)
+
:param unit_type: The type of unit ('A' or 'F')
:param unit_loc: The location of the unit ('BUR', 'BUL/EC')
:param order_type: The type of order ('S' for Support, 'C' for Convoy', '-' for move)
@@ -3422,9 +3578,10 @@ class Game(Jsonable):
def _unit_owner(self, unit, coast_required=1):
""" Finds the power who owns a unit
+
:param unit: The name of the unit to find (e.g. 'A PAR')
:param coast_required: Indicates that the coast is in the unit
- (if 0, you can search for 'F STP' for example, but if 1, you must specify 'F STP/SC')
+ (if 0, you can search for 'F STP' for example, but if 1, you must specify 'F STP/SC')
:return: The power instance who owns the unit or None
"""
# If coast_required is 0 and unit does not contain a '/'
@@ -3435,6 +3592,7 @@ class Game(Jsonable):
def _occupant(self, site, any_coast=0):
""" Finds the occupant of a site
+
:param site: The site name (e.g. "STP")
:param any_coast: Boolean to indicate to return unit on any coast
:return: The unit (e.g. "A STP", "F STP/NC") occupying the site, None otherwise
@@ -3451,6 +3609,7 @@ class Game(Jsonable):
""" This function sets self.combat to a dictionary of dictionaries, specifying each potential destination
for every piece, with the strengths of each unit's attempt to get (or stay) there, and with the givers
of supports that DON'T country dislodgement. (i.e. supports given by the power owning the occupying unit).
+
:return: Nothing, but sets self.combat
"""
# For example, the following orders, all by the same power:
@@ -3483,6 +3642,7 @@ class Game(Jsonable):
def _detect_paradox(self, starting_node, paradox_action, paradox_last_words):
""" Paradox detection algorithm. Start at starting node and move chain to see if node if performing
paradox action
+
:param starting_node: The location (e.g. PAR) where to start the paradox chain
:param paradox_action: The action that would cause a paradox in the chain (e.g. 'S')
:param paradox_last_words: The last words to detect in a order to cause a paradox (e.g. ['F', 'NTH'])
@@ -3511,8 +3671,9 @@ class Game(Jsonable):
def _check_disruptions(self, may_convoy, result, coresult=None):
""" Determines convoy disruptions.
+
:param may_convoy: Contains the dictionary of all convoys that have a chance to succeed
- (e.g. {'A PAR': ['BER', 'MUN']}
+ (e.g. {'A PAR': ['BER', 'MUN']}
:param result: Result to set for the unit if the convoying fleet would be dislodged
(e.g. 'maybe', 'no convoy')
:param coresult: Result to set for the convoyer if the convoying fleet would be dislodged (e.g. 'dislodged')
@@ -3583,6 +3744,7 @@ class Game(Jsonable):
def _boing(self, unit):
""" Mark a unit bounced, and update the combat table to show the unit as
having strength one at its current location
+
:param unit: The unit to bounce (e.g. 'A PAR')
:return: 1
"""
@@ -3669,6 +3831,7 @@ class Game(Jsonable):
def _cut_support(self, unit, direct=0):
""" See if the order made by the unit cuts a support. If so, cut it.
+
:param unit: The unit who is attacking (and cutting support)
:param direct: Boolean Flag - If set, the order must not only be a move, but also a non-convoyed move.
:return: Nothing
@@ -3709,6 +3872,7 @@ class Game(Jsonable):
def _no_effect(self, unit, site):
""" Removes a unit from the combat list of an attack site
+
:param unit: The unit attacking the site (e.g. ['A PAR', []])
:param site: The site being attacked (e.g. 'MAR')
:return: Nothing
@@ -3722,6 +3886,7 @@ class Game(Jsonable):
def _unbounce(self, site):
""" Unbounce any powerful-enough move that can now take the spot being vacated by the dislodger.
+
:param site: The site being attacked
:return: Nothing
"""
@@ -4015,6 +4180,7 @@ class Game(Jsonable):
def _move_results(self):
""" Resolves moves (Movement phase) and returns a list of messages explaining what happened
+
:return: A list of lines for the results file explaining what happened during the phase
"""
# Resolving moves
@@ -4097,6 +4263,7 @@ class Game(Jsonable):
def _other_results(self):
""" Resolves moves (Retreat and Adjustment phase) and returns a list of messages explaining what happened
+
:return: A list of lines for the results file explaining what happened during the phase
"""
# pylint: disable=too-many-statements,too-many-branches,too-many-nested-blocks
@@ -4320,6 +4487,7 @@ class Game(Jsonable):
def _resolve(self):
""" Resolve the current phase
+
:return: A list of strings for the results file explaining how the phase was resolved.
"""
this_phase = self.phase_type
diff --git a/diplomacy/engine/map.py b/diplomacy/engine/map.py
index 65bcdfa..c23b502 100644
--- a/diplomacy/engine/map.py
+++ b/diplomacy/engine/map.py
@@ -30,84 +30,83 @@ UNDETERMINED, POWER, UNIT, LOCATION, COAST, ORDER, MOVE_SEP, OTHER = 0, 1, 2, 3,
MAP_CACHE = {}
-class Map():
- """ MAP Class
-
- Properties:
- - abbrev: Contains the power abbreviation, otherwise defaults to first letter of PowerName
- e.g. {'ENGLISH': 'E'}
- - abuts_cache: Contains a cache of abuts for ['A,'F'] between all locations for orders ['S', 'C', '-']
- e.g. {(A, PAR, -, MAR): 1, ...}
- - aliases: Contains a dict of all the aliases (e.g. full province name to 3 char)
- e.g. {'EAST': 'EAS', 'STP ( /SC )': 'STP/SC', 'FRENCH': 'FRANCE', 'BUDAPEST': 'BUD', 'NOR': 'NWY', ... }
- - centers: Contains a dict of currently owned supply centers for each player
- e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... }
- - convoy_paths: Contains a list of all possible convoys paths bucketed by number of fleets
- format: {nb of fleets: [(START_LOC, {FLEET LOC}, {DEST LOCS})]}
- - dest_with_coasts: Contains a dictionary of locs with all destinations (incl coasts) that can be reached
- e.g. {'PAR': ['BRE', 'PIC', 'BUR', ...], ...}
- - dummies: Indicates the list of powers that are dummies
- e.g. ['FRANCE', 'ITALY']
- - error: Contains a list of errors that the map generated
- e.g. [''DUPLICATE MAP ALIAS OR POWER: JAPAN']
- - files: Contains a list of files that were loaded (e.g. USES keyword)
- e.g. ['standard.map', 'standard.politics', 'standard.geography', 'standard.military']
- - first_year: Indicates the year where the game is starting.
- e.g. 1901
- - flow: List that contains the seasons with the phases
- e.g. ['SPRING:MOVEMENT,RETREATS', 'FALL:MOVEMENT,RETREATS', 'WINTER:ADJUSTMENTS']
- - flow_sign: Indicate the direction of flow (1 is positive, -1 is negative)
- e.g. 1
- - homes: Contains the list of supply centers where units can be built (i.e. assigned at the beginning)
- e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... }
- - inhabits: List that indicates which power have a INHABITS, HOME, or HOMES line
- e.g. ['FRANCE']
- - keywords: Contains a dict of keywords to parse status files and orders
- e.g. {'BUILDS': 'B', '>': '', 'SC': '/SC', 'REMOVING': 'D', 'WAIVED': 'V', 'ATTACK': '', ... }
- - loc_abut: Contains a adjacency list for each province
- e.g. {'LVP': ['CLY', 'edi', 'IRI', 'NAO', 'WAL', 'yor'], ...}
- - loc_coasts: Contains a mapping of all coasts for every location
- e.g. {'PAR': ['PAR'], 'BUL': ['BUL', 'BUL/EC', 'BUL/SC'], ... }
- - loc_name: Dict that indicates the 3 letter name of each location
- e.g. {'GULF OF LYON': 'LYO', 'BREST': 'BRE', 'BUDAPEST': 'BUD', 'RUHR': 'RUH', ... }
- - loc_type: Dict that indicates if each location is 'WATER', 'COAST', 'LAND', or 'PORT'
- e.g. {'MAO': 'WATER', 'SER': 'LAND', 'SYR': 'COAST', 'MOS': 'LAND', 'VEN': 'COAST', ... }
- - locs: List of 3 letter locations (With coasts)
- e.g. ['ADR', 'AEG', 'ALB', 'ANK', 'APU', 'ARM', 'BAL', 'BAR', 'BEL', 'BER', ... ]
- - name: Name of the map (or full path to a custom map file)
- e.g. 'standard' or '/some/path/to/file.map'
- - own_word: Dict to indicate the word used to refer to people living in each power's country
- e.g. {'RUSSIA': 'RUSSIAN', 'FRANCE': 'FRENCH', 'UNOWNED': 'UNOWNED', 'TURKEY': 'TURKISH', ... }
- - owns: List that indicates which power have a OWNS or CENTERS line
- e.g. ['FRANCE']
- - phase: String to indicate the beginning phase of the map
- e.g. 'SPRING 1901 MOVEMENT'
- - phase_abbrev: Dict to indicate the 1 letter abbreviation for each phase
- e.g. {'A': 'ADJUSTMENTS', 'M': 'MOVEMENT', 'R': 'RETREATS'}
- - pow_name: Dict to indicate the power's name
- e.g. {'RUSSIA': 'RUSSIA', 'FRANCE': 'FRANCE', 'TURKEY': 'TURKEY', 'GERMANY': 'GERMANY', ... }
- - powers: Contains the list of powers (players) in the game
- e.g. ['AUSTRIA', 'ENGLAND', 'FRANCE', 'GERMANY', 'ITALY', 'RUSSIA', 'TURKEY']
- - root_map: Contains the name of the original map file loaded (before the USES keyword are applied)
- A map that is called with MAP is the root_map
- e.g. 'standard'
- - rules: Contains a list of rules used by all variants (for display only)
- e.g. ['RULE_1']
- - scs: Contains a list of all the supply centers in the game
- e.g. ['MOS', 'SEV', 'STP', 'WAR', 'BRE', 'MAR', 'PAR', 'BEL', 'BUL', 'DEN', 'GRE', 'HOL', 'NWY', ... ]
- - seq: [] Contains the sequence of seasons in format 'SEASON_NAME SEASON_TYPE'
- e.g. ['NEWYEAR', 'SPRING MOVEMENT', 'SPRING RETREATS', 'FALL MOVEMENT', 'FALL RETREATS',
- 'WINTER ADJUSTMENTS']
- - unclear: Contains the alias for ambiguous places
- e.g. {'EAST': 'EAS'}
- - unit_names: {} Contains a dict of the unit names
- e.g. {'F': 'FLEET', 'A': 'ARMY'}
- - units: Dict that contains the current position of each unit by power
- e.g. {'FRANCE': ['F BRE', 'A MAR', 'A PAR'], 'RUSSIA': ['A WAR', 'A MOS', 'F SEV', 'F STP/SC'], ... }
- - validated: Boolean to indicate if the map file has been validated
- e.g. 1
- - victory: Indicates the number of supply centers to win the game (>50% required if None)
- e.g. 18
+class Map:
+ """ Map Class
+
+ Properties:
+
+ - **abbrev**: Contains the power abbreviation, otherwise defaults to first letter of PowerName
+ e.g. {'ENGLISH': 'E'}
+ - **abuts_cache**: Contains a cache of abuts for ['A,'F'] between all locations for orders ['S', 'C', '-']
+ e.g. {(A, PAR, -, MAR): 1, ...}
+ - **aliases**: Contains a dict of all the aliases (e.g. full province name to 3 char)
+ e.g. {'EAST': 'EAS', 'STP ( /SC )': 'STP/SC', 'FRENCH': 'FRANCE', 'BUDAPEST': 'BUD', 'NOR': 'NWY', ... }
+ - **centers**: Contains a dict of owned supply centers for each player at the beginning of the map
+ e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... }
+ - **convoy_paths**: Contains a list of all possible convoys paths bucketed by number of fleets
+ format: {nb of fleets: [(START_LOC, {FLEET LOC}, {DEST LOCS})]}
+ - **dest_with_coasts**: Contains a dictionary of locs with all destinations (incl coasts) that can be reached
+ e.g. {'PAR': ['BRE', 'PIC', 'BUR', ...], ...}
+ - **dummies**: Indicates the list of powers that are dummies
+ e.g. ['FRANCE', 'ITALY']
+ - **error**: Contains a list of errors that the map generated
+ e.g. [''DUPLICATE MAP ALIAS OR POWER: JAPAN']
+ - **files**: Contains a list of files that were loaded (e.g. USES keyword)
+ e.g. ['standard.map', 'standard.politics', 'standard.geography', 'standard.military']
+ - **first_year**: Indicates the year where the game is starting.
+ e.g. 1901
+ - **flow**: List that contains the seasons with the phases
+ e.g. ['SPRING:MOVEMENT,RETREATS', 'FALL:MOVEMENT,RETREATS', 'WINTER:ADJUSTMENTS']
+ - **flow_sign**: Indicate the direction of flow (1 is positive, -1 is negative)
+ e.g. 1
+ - **homes**: Contains the list of supply centers where units can be built (i.e. assigned at the beginning)
+ e.g. {'RUSSIA': ['MOS', 'SEV', 'STP', 'WAR'], 'FRANCE': ['BRE', 'MAR', 'PAR'], ... }
+ - **inhabits**: List that indicates which power have a INHABITS, HOME, or HOMES line
+ e.g. ['FRANCE']
+ - **keywords**: Contains a dict of keywords to parse status files and orders
+ e.g. {'BUILDS': 'B', '>': '', 'SC': '/SC', 'REMOVING': 'D', 'WAIVED': 'V', 'ATTACK': '', ... }
+ - **loc_abut**: Contains a adjacency list for each province
+ e.g. {'LVP': ['CLY', 'edi', 'IRI', 'NAO', 'WAL', 'yor'], ...}
+ - **loc_coasts**: Contains a mapping of all coasts for every location
+ e.g. {'PAR': ['PAR'], 'BUL': ['BUL', 'BUL/EC', 'BUL/SC'], ... }
+ - **loc_name**: Dict that indicates the 3 letter name of each location
+ e.g. {'GULF OF LYON': 'LYO', 'BREST': 'BRE', 'BUDAPEST': 'BUD', 'RUHR': 'RUH', ... }
+ - **loc_type**: Dict that indicates if each location is 'WATER', 'COAST', 'LAND', or 'PORT'
+ e.g. {'MAO': 'WATER', 'SER': 'LAND', 'SYR': 'COAST', 'MOS': 'LAND', 'VEN': 'COAST', ... }
+ - **locs**: List of 3 letter locations (With coasts)
+ e.g. ['ADR', 'AEG', 'ALB', 'ANK', 'APU', 'ARM', 'BAL', 'BAR', 'BEL', 'BER', ... ]
+ - **name**: Name of the map (or full path to a custom map file)
+ e.g. 'standard' or '/some/path/to/file.map'
+ - **own_word**: Dict to indicate the word used to refer to people living in each power's country
+ e.g. {'RUSSIA': 'RUSSIAN', 'FRANCE': 'FRENCH', 'UNOWNED': 'UNOWNED', 'TURKEY': 'TURKISH', ... }
+ - **owns**: List that indicates which power have a OWNS or CENTERS line
+ e.g. ['FRANCE']
+ - **phase**: String to indicate the beginning phase of the map
+ e.g. 'SPRING 1901 MOVEMENT'
+ - **phase_abbrev**: Dict to indicate the 1 letter abbreviation for each phase
+ e.g. {'A': 'ADJUSTMENTS', 'M': 'MOVEMENT', 'R': 'RETREATS'}
+ - **pow_name**: Dict to indicate the power's name
+ e.g. {'RUSSIA': 'RUSSIA', 'FRANCE': 'FRANCE', 'TURKEY': 'TURKEY', 'GERMANY': 'GERMANY', ... }
+ - **powers**: Contains the list of powers (players) in the game
+ e.g. ['AUSTRIA', 'ENGLAND', 'FRANCE', 'GERMANY', 'ITALY', 'RUSSIA', 'TURKEY']
+ - **root_map**: Contains the name of the original map file loaded (before the USES keyword are applied)
+ A map that is called with MAP is the root_map. e.g. 'standard'
+ - **rules**: Contains a list of rules used by all variants (for display only)
+ e.g. ['RULE_1']
+ - **scs**: Contains a list of all the supply centers in the game
+ e.g. ['MOS', 'SEV', 'STP', 'WAR', 'BRE', 'MAR', 'PAR', 'BEL', 'BUL', 'DEN', 'GRE', 'HOL', 'NWY', ... ]
+ - **seq**: [] Contains the sequence of seasons in format 'SEASON_NAME SEASON_TYPE'
+ e.g. ['NEWYEAR', 'SPRING MOVEMENT', 'SPRING RETREATS', 'FALL MOVEMENT', 'FALL RETREATS', 'WINTER ADJUSTMENTS']
+ - **unclear**: Contains the alias for ambiguous places
+ e.g. {'EAST': 'EAS'}
+ - **unit_names**: {} Contains a dict of the unit names
+ e.g. {'F': 'FLEET', 'A': 'ARMY'}
+ - **units**: Dict that contains the current position of each unit by power
+ e.g. {'FRANCE': ['F BRE', 'A MAR', 'A PAR'], 'RUSSIA': ['A WAR', 'A MOS', 'F SEV', 'F STP/SC'], ... }
+ - **validated**: Boolean to indicate if the map file has been validated
+ e.g. 1
+ - **victory**: Indicates the number of supply centers to win the game (>50% required if None)
+ e.g. 18
"""
# pylint: disable=too-many-instance-attributes
@@ -119,6 +118,7 @@ class Map():
def __new__(cls, name='standard', use_cache=True):
""" New function - Retrieving object from cache if possible
+
:param name: Name of the map to load
:param use_cache: Boolean flag to indicate we want a blank object that doesn't use cache
"""
@@ -128,6 +128,7 @@ class Map():
def __init__(self, name='standard', use_cache=True):
""" Constructor function
+
:param name: Name of the map to load (or full path to a custom map file)
:param use_cache: Boolean flag to indicate we want a blank object that doesn't use cache
"""
@@ -168,8 +169,18 @@ class Map():
def __str__(self):
return self.name
+ @property
+ def svg_path(self):
+ """ Return path to the SVG file of this map (or None if it does not exist) """
+ for file_name in [self.name + '.svg', self.root_map + '.svg']:
+ svg_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', file_name)
+ if os.path.exists(svg_path):
+ return svg_path
+ return None
+
def validate(self, force=0):
""" Validate that the configuration from a map file is correct
+
:param force: Indicate that we want to force a validation, even if the map is already validated
:return: Nothing
"""
@@ -294,6 +305,7 @@ class Map():
def load(self, file_name=None):
""" Loads a map file from disk
+
:param file_name: Optional. A string representing the file to open. Otherwise, defaults to the map name
:return: Nothing
"""
@@ -716,8 +728,9 @@ class Map():
def add_homes(self, power, homes, reinit):
""" Add new homes (and deletes previous homes if reinit)
+
:param power: Name of power (e.g. ITALY)
- :param homes: List of homes e.g. ['BUR', '-POR', '*ITA', ... ]
+ :param homes: List of homes e.g. ``['BUR', '-POR', '*ITA', ... ]``
:param reinit: Indicates that we want to strip the list of homes before adding
:return: Nothing
"""
@@ -753,6 +766,7 @@ class Map():
def drop(self, place):
""" Drop a place
+
:param place: Name of place to remove
:return: Nothing
"""
@@ -807,6 +821,7 @@ class Map():
def norm_power(self, power):
""" Normalise the name of a power (removes spaces)
+
:param power: Name of power to normalise
:return: Normalised power name
"""
@@ -814,6 +829,7 @@ class Map():
def norm(self, phrase):
""" Normalise a sentence (add spaces before /, replace -+, with ' ', remove .:
+
:param phrase: Phrase to normalise
:return: Normalised sentences
"""
@@ -828,8 +844,9 @@ class Map():
def compact(self, phrase):
""" Compacts a full sentence into a list of short words
+
:param phrase: The full sentence to compact (e.g. 'England: Fleet Western Mediterranean -> Tyrrhenian
- Sea. (*bounce*)')
+ Sea. (*bounce*)')
:return: The compacted phrase in an array (e.g. ['ENGLAND', 'F', 'WES', 'TYS', '|'])
"""
if ':' in phrase:
@@ -849,6 +866,7 @@ class Map():
def alias(self, word):
""" This function is used to replace multi-words with their acronyms
+
:param word: The current list of words to try to shorten
:return: alias, ix - alias is the shorten list of word, ix is the ix of the next non-processed word
"""
@@ -922,12 +940,20 @@ class Map():
def vet(self, word, strict=0):
""" Determines the type of every word in a compacted order phrase
- 0 - Undetermined, 1 - Power, 2 - Unit, 3 - Location, 4 - Coastal location
- 5 - Order, 6 - Move Operator (-=_^), 7 - Non-move separator (|?~) or result (*!?~+)
- :param word: The list of words to vet (e.g. ['A', 'POR', 'S', 'SPA/NC'])
+
+ 0 - Undetermined,
+ 1 - Power,
+ 2 - Unit,
+ 3 - Location,
+ 4 - Coastal location
+ 5 - Order,
+ 6 - Move Operator ``(-=_^)``,
+ 7 - Non-move separator ``(|?~)`` or result ``(*!?~+)``
+
+ :param word: The list of words to vet (e.g. ``['A', 'POR', 'S', 'SPA/NC']``)
:param strict: Boolean to indicate that we want to verify that the words actually exist.
Numbers become negative if they don't exist
- :return: A list of tuple (e.g. [('A', 2), ('POR', 3), ('S', 5), ('SPA/NC', 4)])
+ :return: A list of tuple (e.g. ``[('A', 2), ('POR', 3), ('S', 5), ('SPA/NC', 4)]``)
"""
result = []
for thing in word:
@@ -960,6 +986,7 @@ class Map():
def rearrange(self, word):
""" This function is used to parse commands
+
:param word: The list of words to vet (e.g. ['ENGLAND', 'F', 'WES', 'TYS', '|'])
:return: The list of words in the correct order to be processed (e.g. ['ENGLAND', 'F', 'WES', '-', 'TYS'])
"""
@@ -1070,6 +1097,7 @@ class Map():
def area_type(self, loc):
""" Returns 'WATER', 'COAST', 'PORT', 'LAND', 'SHUT'
+
:param loc: The name of the location to query
:return: Type of the location ('WATER', 'COAST', 'PORT', 'LAND', 'SHUT')
"""
@@ -1078,6 +1106,7 @@ class Map():
def default_coast(self, word):
""" Returns the coast for a fleet move order that can only be to a single coast
(e.g. F GRE-BUL returns F GRE-BUL/SC)
+
:param word: A list of tokens (e.g. ['F', 'GRE', '-', 'BUL'])
:return: The updated list of tokens (e.g. ['F', 'GRE', '-', 'BUL/SC'])
"""
@@ -1096,14 +1125,16 @@ class Map():
def find_coasts(self, loc):
""" Finds all coasts for a given location
+
:param loc: The name of a location (e.g. 'BUL')
:return: Returns the list of all coasts, including the location (e.g. ['BUL', 'BUL/EC', 'BUL/SC']
"""
return self.loc_coasts.get(loc.upper(), [])
def abuts(self, unit_type, unit_loc, order_type, other_loc):
- """ Determines if a order for unit_type from unit_loc to other_loc is adjacent
- Note: This method uses the precomputed cache
+ """ Determines if a order for unit_type from unit_loc to other_loc is adjacent.
+
+ **Note**: This method uses the precomputed cache
:param unit_type: The type of unit ('A' or 'F')
:param unit_loc: The location of the unit ('BUR', 'BUL/EC')
@@ -1120,7 +1151,8 @@ class Map():
def _abuts(self, unit_type, unit_loc, order_type, other_loc):
""" Determines if a order for unit_type from unit_loc to other_loc is adjacent
- Note: This method is used to generate the abuts_cache
+
+ **Note**: This method is used to generate the abuts_cache
:param unit_type: The type of unit ('A' or 'F')
:param unit_loc: The location of the unit ('BUR', 'BUL/EC')
@@ -1186,6 +1218,7 @@ class Map():
def is_valid_unit(self, unit, no_coast_ok=0, shut_ok=0):
""" Determines if a unit and location combination is valid (e.g. 'A BUR') is valid
+
:param unit: The name of the unit with its location (e.g. F SPA/SC)
:param no_coast_ok: Indicates if a coastal location with no coast (e.g. SPA vs SPA/SC) is acceptable
:param shut_ok: Indicates if a impassable country (e.g. Switzerland) is OK
@@ -1209,12 +1242,17 @@ class Map():
def abut_list(self, site, incl_no_coast=False):
""" Returns the adjacency list for the site
+
:param site: The province we want the adjacency list for
:param incl_no_coast: Boolean flag that indicates to also include province without coast if it has coasts
- e.g. will return ['BUL/SC', 'BUL/EC'] if False, and ['bul', 'BUL/SC', 'BUL/EC'] if True
+ e.g. will return ['BUL/SC', 'BUL/EC'] if False, and ['bul', 'BUL/SC', 'BUL/EC'] if True
:return: A list of adjacent provinces
- Note: abuts are returned in mixed cases (lowercase for A only, First capital letter for F only)
+ Note: abuts are returned in **mixed cases**
+
+ - An adjacency that is lowercase (e.g. 'bur') can only be used by an army
+ - An adjacency that starts with a capital letter (e.g. 'Bal') can only be used by a fleet
+ - An adjacency that is uppercase can be used by both an army and a fleet
"""
if site in self.loc_abut:
abut_list = self.loc_abut.get(site, [])
@@ -1229,9 +1267,10 @@ class Map():
def find_next_phase(self, phase, phase_type=None, skip=0):
""" Returns the long name of the phase coming immediately after the phase
+
:param phase: The long name of the current phase (e.g. SPRING 1905 RETREATS)
- :param phase_type: The type of phase we are looking for (e.g. 'M' for Movement, 'R' for Retreats,
- 'A' for Adjust.)
+ :param phase_type: The type of phase we are looking for
+ (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.)
:param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after)
:return: The long name of the next phase (e.g. FALL 1905 MOVEMENT)
"""
@@ -1277,9 +1316,10 @@ class Map():
def find_previous_phase(self, phase, phase_type=None, skip=0):
""" Returns the long name of the phase coming immediately prior the phase
+
:param phase: The long name of the current phase (e.g. SPRING 1905 RETREATS)
- :param phase_type: The type of phase we are looking for (e.g. 'M' for Movement, 'R' for Retreats,
- 'A' for Adjust.)
+ :param phase_type: The type of phase we are looking for
+ (e.g. 'M' for Movement, 'R' for Retreats, 'A' for Adjust.)
:param skip: The number of match to skip (e.g. 1 to find not the next phase, but the one after)
:return: The long name of the previous phase (e.g. SPRING 1905 MOVEMENT)
"""
@@ -1332,6 +1372,7 @@ class Map():
def compare_phases(self, phase1, phase2):
""" Compare 2 phases (Strings) and return 1, -1, or 0 to indicate which phase is larger
+
:param phase1: The first phase (e.g. S1901M, FORMING, COMPLETED)
:param phase2: The second phase (e.g. S1901M, FORMING, COMPLETED)
:return: 1 if phase1 > phase2, -1 if phase2 > phase1 otherwise 0 if they are equal
@@ -1378,6 +1419,7 @@ class Map():
@staticmethod
def phase_abbr(phase, default='?????'):
""" Constructs a 5 character representation (S1901M) from a phase (SPRING 1901 MOVEMENT)
+
:param phase: The full phase (e.g. SPRING 1901 MOVEMENT)
:param default: The default value to return in case conversion fails
:return: A 5 character representation of the phase
@@ -1389,6 +1431,7 @@ class Map():
def phase_long(self, phase_abbr, default='?????'):
""" Constructs a full sentence of a phase from a 5 character abbreviation
+
:param phase_abbr: 5 character abbrev. (e.g. S1901M)
:param default: The default value to return in case conversion fails
:return: A full phase description (e.g. SPRING 1901 MOVEMENT)
diff --git a/diplomacy/engine/message.py b/diplomacy/engine/message.py
index 150efd4..8094bfe 100644
--- a/diplomacy/engine/message.py
+++ b/diplomacy/engine/message.py
@@ -17,6 +17,7 @@
""" Game message. Represent a message exchanged inside a game.
Possible messages exchanges:
+
- power 1 -> power 2
- power -> all game
- system -> power
@@ -25,10 +26,12 @@
- system -> omniscient observers
Sender `system` is identified with constant SYSTEM defined below.
- Recipients `all game`, `observers` and `omniscient observers` are identified respectively with constants
- GLOBAL, OBSERVER and OMNISCIENT defined below.
+
+ Recipients `all game`, `observers` and `omniscient observers` are identified respectively with
+ constants GLOBAL, OBSERVER and OMNISCIENT defined below.
Consider using Game methods to generate appropriate messages instead of this class directly:
+
- Game.new_power_message() to send a message from a power to another.
- Game.new_global_message() to send a message from a power to all game.
- ServerGame.new_system_message() to send a server system message.
@@ -44,14 +47,18 @@ OBSERVER = 'OBSERVER' # recipient (all observer tokens)
OMNISCIENT = 'OMNISCIENT' # recipient (all omniscient tokens)
class Message(Jsonable):
- """ GameMessage class. Properties:
- - sender: message sender name: either SYSTEM or a power name.
- - recipient: message recipient name: either GLOBAL, OBSERVER, OMNISCIENT or a power name.
- - time_sent: message timestamp in microseconds.
- - phase: short name of game phase when message is sent.
- - message: message body.
-
- Note about timestamp management:
+ """ Message class.
+
+ Properties:
+
+ - **sender**: message sender name: either SYSTEM or a power name.
+ - **recipient**: message recipient name: either GLOBAL, OBSERVER, OMNISCIENT or a power name.
+ - **time_sent**: message timestamp in microseconds.
+ - **phase**: short name of game phase when message is sent.
+ - **message**: message body.
+
+ **Note about timestamp management**:
+
We assume a message has an unique timestamp inside one game. To respect this rule, the server is the only one
responsible for generating message timestamps. This allow to generate timestamp or only 1 same machine (server)
instead of managing timestamps from many user machines, to prevent timestamp inconsistency when messages
@@ -60,19 +67,19 @@ class Message(Jsonable):
"""
__slots__ = ['sender', 'recipient', 'time_sent', 'phase', 'message']
model = {
- strings.SENDER: str, # either SYSTEM or a power name.
- strings.RECIPIENT: str, # either GLOBAL, OBSERVER, OMNISCIENT or a power name.
+ strings.SENDER: str, # either SYSTEM or a power name.
+ strings.RECIPIENT: str, # either GLOBAL, OBSERVER, OMNISCIENT or a power name.
strings.TIME_SENT: parsing.OptionalValueType(int), # given by server.
- strings.PHASE: str, # phase short name.
+ strings.PHASE: str, # phase short name (e.g. 'S1901M' or 'COMPLETED')
strings.MESSAGE: str,
}
def __init__(self, **kwargs):
- self.sender = None # type: str
- self.recipient = None # type: str
- self.time_sent = None # type: int
- self.phase = None # type: str
- self.message = None # type: str
+ self.sender = None # type: str
+ self.recipient = None # type: str
+ self.time_sent = None # type: int
+ self.phase = None # type: str
+ self.message = None # type: str
super(Message, self).__init__(**kwargs)
def __str__(self):
diff --git a/diplomacy/engine/power.py b/diplomacy/engine/power.py
index 1fe282b..c3826e6 100644
--- a/diplomacy/engine/power.py
+++ b/diplomacy/engine/power.py
@@ -15,6 +15,7 @@
# with this program. If not, see <https://www.gnu.org/licenses/>.
# ==============================================================================
""" Power
+
- Contains the power object representing a power in the game
"""
from copy import deepcopy
@@ -28,33 +29,36 @@ from diplomacy.utils.constants import OrderSettings
class Power(Jsonable):
""" Power Class
- Properties:
- - abbrev - Contains the abbrev of the power (usually the first letter of the power name) (e.g. 'F' for FRANCE)
- - adjust - List of pending adjustment orders
- (e.g. ['A PAR B', 'A PAR R MAR', 'A MAR D', 'WAIVE'])
- - centers - Contains the list of supply centers currently controlled by the power ['MOS', 'SEV', 'STP', 'WAR']
- - civil_disorder - Boolean flag to indicate that the power has been put in CIVIL_DISORDER (e.g. True or False)
- - controller - Sorted dictionary mapping timestamp to controller (either dummy or a user ID) who takes
- control of power at this timestamp.
- - game - Contains a reference to the game object
- - goner - Boolean to indicate that this power doesn't control any SCs any more (e.g. True or False)
- - homes - Contains a list of homes supply centers (where you can build)
- e.g. ['PAR', 'MAR', ... ] or None if empty
- - influence - Contains a list of locations influenced by this power
- Note: To influence a location, the power must have visited it last.
- e.g ['PAR', 'MAR', ... ]
- - name - Contains the name of the power
- - orders - Contains a dictionary of units and their orders.
- For NO_CHECK games, unit is 'ORDER 1', 'ORDER 2', ...
- - e.g. {'A PAR': '- MAR' } or {'ORDER 1': 'A PAR - MAR', 'ORDER 2': '...', ... }
- - Can also be {'REORDER 1': 'A PAR - MAR', 'INVALID 1': 'A PAR - MAR', ... } after validation
- - retreats - Contains the list of units that need to retreat with their possible retreat locations
- (e.g. {'A PAR': ['MAR', 'BER']})
- - role - Power type (observer, omniscient, player or server power).
- Either the power name (for a player power) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES
- - tokens - Only for server power: set of tokens of current power controlled (if not None).
- - units - Contains the list of units (e.g. ['A PAR', 'A MAR', ...]
- - vote - Only for omniscient, player and server power: power vote ('yes', 'no' or 'neutral').
+ Properties:
+
+ - **abbrev** - Contains the abbrev of the power (i.e. the first letter of the power name) (e.g. 'F' for FRANCE)
+ - **adjust** - List of pending adjustment orders
+ (e.g. ['A PAR B', 'A PAR R MAR', 'A MAR D', 'WAIVE'])
+ - **centers** - Contains the list of supply centers currently controlled by the power ['MOS', 'SEV', 'STP']
+ - **civil_disorder** - Bool flag to indicate that the power has been put in CIVIL_DISORDER (e.g. True or False)
+ - **controller** - Sorted dictionary mapping timestamp to controller (either dummy or a user ID) who takes
+ control of power at this timestamp.
+ - **game** - Contains a reference to the game object
+ - **goner** - Boolean to indicate that this power doesn't control any SCs any more (e.g. True or False)
+ - **homes** - Contains a list of homes supply centers (where you can build)
+ e.g. ['PAR', 'MAR', ... ] or None if empty
+ - **influence** - Contains a list of locations influenced by this power
+ Note: To influence a location, the power must have visited it last.
+ e.g ['PAR', 'MAR', ... ]
+ - **name** - Contains the name of the power (e.g. 'FRANCE')
+ - **orders** - Contains a dictionary of units and their orders.
+ For NO_CHECK games, unit is 'ORDER 1', 'ORDER 2', ...
+
+ - e.g. {'A PAR': '- MAR' } or {'ORDER 1': 'A PAR - MAR', 'ORDER 2': '...', ... }
+ - Can also be {'REORDER 1': 'A PAR - MAR', 'INVALID 1': 'A PAR - MAR', ... } after validation
+
+ - **retreats** - Contains the list of units that need to retreat with their possible retreat locations
+ (e.g. {'A PAR': ['MAR', 'BER']})
+ - **role** - Power type (observer, omniscient, player or server power).
+ Either the power name (for a player power) or a value in diplomacy.utils.strings.ALL_ROLE_TYPES
+ - **tokens** - Only for server power: set of tokens of current power controlled (if not None).
+ - **units** - Contains the list of units (e.g. ['A PAR', 'A MAR', ...]
+ - **vote** - Only for omniscient, player and server power: power vote ('yes', 'no' or 'neutral').
"""
__slots__ = ['game', 'name', 'abbrev', 'adjust', 'centers', 'units', 'influence', 'homes',
'retreats', 'goner', 'civil_disorder', 'orders', 'role', 'controller', 'vote',
@@ -128,9 +132,7 @@ class Power(Jsonable):
return text
def __deepcopy__(self, memo):
- """ Fast deep copy implementation
- - (Not setting the game object)
- """
+ """ Fast deep copy implementation (**not setting the game object**) """
cls = self.__class__
result = cls.__new__(cls)
@@ -145,6 +147,7 @@ class Power(Jsonable):
def reinit(self, include_flags=6):
""" Performs a reinitialization of some of the parameters
+
:param include_flags: Bit mask to indicate which params to reset
(bit 1 = orders, 2 = persistent, 4 = transient)
:return: None
@@ -187,6 +190,7 @@ class Power(Jsonable):
@staticmethod
def compare(power_1, power_2):
""" Comparator object - Compares two Power objects
+
:param power_1: The first Power object to compare
:param power_2: The second Power object to compare
:return: 1 if self is greater, -1 if other is greater, 0 if they are equal
@@ -199,6 +203,7 @@ class Power(Jsonable):
def initialize(self, game):
""" Initializes a game and resets home, centers and units
+
:param game: The game to use for initialization
:type game: diplomacy.Game
"""
@@ -233,6 +238,7 @@ class Power(Jsonable):
def merge(self, other_power):
""" Transfer all units, centers, and homes of the other_power to this power
+
:param other_power: The other power (will be empty after the merge)
"""
# Regular units
@@ -291,12 +297,14 @@ class Power(Jsonable):
def is_dummy(self):
""" Indicates if the power is a dummy
+
:return: Boolean flag to indicate if the power is a dummy
"""
return self.controller.last_value() == strings.DUMMY
def is_eliminated(self):
""" Returns a flag to show if player is eliminated
+
:return: If the current power is eliminated
"""
# Not eliminated if has units left
@@ -310,6 +318,7 @@ class Power(Jsonable):
def moves_submitted(self):
""" Returns a boolean to indicate if moves has been submitted
+
:return: 1 if not in Movement phase, or orders submitted, or no more units lefts
"""
if self.game.phase_type != 'M':
@@ -321,35 +330,37 @@ class Power(Jsonable):
# ==============================================================
def is_observer_power(self):
- """ Return True if this power is an observer power. """
+ """ (Network Method) Return True if this power is an observer power. """
return self.role == strings.OBSERVER_TYPE
def is_omniscient_power(self):
- """ Return True if this power is an omniscient power. """
+ """ (Network Method) Return True if this power is an omniscient power. """
return self.role == strings.OMNISCIENT_TYPE
def is_player_power(self):
- """ Return True if this power is a player power. """
+ """ (Network Method) Return True if this power is a player power. """
return self.role == self.name
def is_server_power(self):
- """ Return True if this power is a server power. """
+ """ (Network Method) Return True if this power is a server power. """
return self.role == strings.SERVER_TYPE
def is_controlled(self):
- """ Return True if this power is controlled. """
+ """ (Network Method) Return True if this power is controlled. """
return self.controller.last_value() != strings.DUMMY
def does_not_wait(self):
- """ Return True if this power does not wait (ie. if we could already process orders of this power). """
+ """ (Network Method) Return True if this power does not wait
+ (ie. if we could already process orders of this power).
+ """
return self.order_is_set and not self.wait
def update_controller(self, username, timestamp):
- """ Update controller with given username and timestamp. """
+ """ (Network Method) Update controller with given username and timestamp. """
self.controller.put(timestamp, username)
def set_controlled(self, username):
- """ Control power with given username. Username may be None (meaning no controller). """
+ """ (Network Method) Control power with given username. Username may be None (meaning no controller). """
if username is None or username == strings.DUMMY:
if self.controller.last_value() != strings.DUMMY:
self.controller.put(common.timestamp_microseconds(), strings.DUMMY)
@@ -363,15 +374,15 @@ class Power(Jsonable):
raise DiplomacyException('Power already controlled by someone else. Kick previous controller before.')
def get_controller(self):
- """ Return current power controller name ('dummy' if power is not controlled). """
+ """ (Network Method) Return current power controller name ('dummy' if power is not controlled). """
return self.controller.last_value()
def get_controller_timestamp(self):
- """ Return timestamp when current controller took control of this power. """
+ """ (Network Method) Return timestamp when current controller took control of this power. """
return self.controller.last_key()
def is_controlled_by(self, username):
- """ Return True if this power is controlled by given username. """
+ """ (Network Method) Return True if this power is controlled by given username. """
if username == constants.PRIVATE_BOT_USERNAME:
# Bot is connected if power is dummy and has some associated tokens.
return self.is_dummy() and bool(self.tokens)
@@ -380,16 +391,16 @@ class Power(Jsonable):
# Server-only methods.
def has_token(self, token):
- """ Return True if this power has given token. """
+ """ (Server Method) Return True if this power has given token. """
assert self.is_server_power()
return token in self.tokens
def add_token(self, token):
- """ Add given token to this power. """
+ """ (Server Method) Add given token to this power. """
assert self.is_server_power()
self.tokens.add(token)
def remove_tokens(self, tokens):
- """ Remove sequence of tokens from this power. """
+ """ (Server Method) Remove sequence of tokens from this power. """
assert self.is_server_power()
self.tokens.difference_update(tokens)
diff --git a/diplomacy/engine/renderer.py b/diplomacy/engine/renderer.py
index 1c92886..e4c6ff3 100644
--- a/diplomacy/engine/renderer.py
+++ b/diplomacy/engine/renderer.py
@@ -16,6 +16,7 @@
# ==============================================================================
# -*- coding: utf-8 -*-
""" Renderer
+
- Contains the renderer object which is responsible for rendering a game state to svg
"""
import os
@@ -35,14 +36,16 @@ def _attr(node_element, attr_name):
""" Shorthand method to retrieve an XML attribute """
return node_element.attributes[attr_name].value
-class Renderer():
+class Renderer:
""" Renderer object responsible for rendering a game state to svg """
def __init__(self, game, svg_path=None):
""" Constructor
+
:param game: The instantiated game object to render
:param svg_path: Optional. Can be set to the full path of a custom SVG to use for rendering the map.
:type game: diplomacy.Game
+ :type svg_path: str, optional
"""
self.game = game
self.metadata = {}
@@ -50,33 +53,25 @@ class Renderer():
# If no SVG path provided, we default to the one in the maps folder
if not svg_path:
- svg_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', self.game.map.name + '.svg')
+ for file_name in [self.game.map.name + '.svg', self.game.map.root_map + '.svg']:
+ svg_path = os.path.join(settings.PACKAGE_DIR, 'maps', 'svg', file_name)
+ if os.path.exists(svg_path):
+ break
# Loading XML
if os.path.exists(svg_path):
self.xml_map = minidom.parse(svg_path).toxml()
self._load_metadata()
- def norm_order(self, order):
- """ Normalizes the order format and split it into tokens
- This is only used for **movement** orders (to make sure NO_CHECK games used the correct format)
- Formats:
- A PAR H
- A PAR - BUR [VIA]
- A PAR S BUR
- A PAR S F BRE - PIC
- F BRE C A PAR - LON
-
- :param order: The unformatted order (e.g. 'Paris - Burgundy')
- :return: The tokens of the formatted order (e.g. ['A', 'PAR', '-', 'BUR'])
- """
- return self.game._add_unit_types(self.game._expand_order(order.split())) # pylint: disable=protected-access
-
def render(self, incl_orders=True, incl_abbrev=False, output_format='svg'):
""" Renders the current game and returns the XML representation
+
:param incl_orders: Optional. Flag to indicate we also want to render orders.
:param incl_abbrev: Optional. Flag to indicate we also want to display the provinces abbreviations.
- :param output_format: The desired output format.
+ :param output_format: The desired output format. Valid values are: 'svg'
+ :type incl_orders: bool, optional
+ :type incl_abbrev: bool, optional
+ :type output_format: str, optional
:return: The rendered image in the specified format.
"""
# pylint: disable=too-many-branches
@@ -127,7 +122,7 @@ class Renderer():
order = '{} {}'.format(order_key, power.orders[order_key])
# Normalizing and splitting in tokens
- tokens = self.norm_order(order)
+ tokens = self._norm_order(order)
unit_loc = tokens[1]
# Parsing based on order type
@@ -240,8 +235,26 @@ class Renderer():
svg_node.removeChild(xml_map.getElementsByTagName('jdipNS:PROVINCE_DATA')[0])
self.xml_map = xml_map.toxml()
+ def _norm_order(self, order):
+ """ Normalizes the order format and split it into tokens
+ This is only used for **movement** orders (to make sure NO_CHECK games used the correct format)
+
+ Formats: ::
+
+ A PAR H
+ A PAR - BUR [VIA]
+ A PAR S BUR
+ A PAR S F BRE - PIC
+ F BRE C A PAR - LON
+
+ :param order: The unformatted order (e.g. 'Paris - Burgundy')
+ :return: The tokens of the formatted order (e.g. ['A', 'PAR', '-', 'BUR'])
+ """
+ return self.game._add_unit_types(self.game._expand_order(order.split())) # pylint: disable=protected-access
+
def _add_unit(self, xml_map, unit, power_name, is_dislodged):
""" Adds a unit to the map
+
:param xml_map: The xml map being generated
:param unit: The unit to add (e.g. 'A PAR')
:param power_name: The name of the power owning the unit (e.g. 'FRANCE')
@@ -271,6 +284,7 @@ class Renderer():
def _set_influence(self, xml_map, loc, power_name, has_supply_center=False):
""" Sets the influence on the map
+
:param xml_map: The xml map being generated
:param loc: The province being influenced (e.g. 'PAR')
:param power_name: The name of the power influencing the province
@@ -317,6 +331,7 @@ class Renderer():
@staticmethod
def _set_current_phase(xml_map, current_phase):
""" Sets the phase text at the bottom right of the the map
+
:param xml_map: The xml map being generated
:param current_phase: The current phase (e.g. 'S1901M)
:return: Nothing
@@ -331,6 +346,7 @@ class Renderer():
@staticmethod
def _set_note(xml_map, note_1, note_2):
""" Sets a note at the top left of the map
+
:param xml_map: The xml map being generated
:param note_1: The text to display on the first line
:param note_2: The text to display on the second line
@@ -347,6 +363,7 @@ class Renderer():
def _issue_hold_order(self, xml_map, loc, power_name):
""" Adds a hold order to the map
+
:param xml_map: The xml map being generated
:param loc: The province where the unit is holding (e.g. 'PAR')
:param power_name: The name of the power owning the unit
@@ -380,6 +397,7 @@ class Renderer():
def _issue_support_hold_order(self, xml_map, loc, dest_loc, power_name):
""" Issues a support hold order
+
:param xml_map: The xml map being generated
:param loc: The location of the unit sending support (e.g. 'BER')
:param dest_loc: The location where the unit is holding from (e.g. 'PAR')
@@ -442,6 +460,7 @@ class Renderer():
def _issue_move_order(self, xml_map, src_loc, dest_loc, power_name):
""" Issues a move order
+
:param xml_map: The xml map being generated
:param src_loc: The location where the unit is moving from (e.g. 'PAR')
:param dest_loc: The location where the unit is moving to (e.g. 'MAR')
@@ -501,6 +520,7 @@ class Renderer():
def _issue_support_move_order(self, xml_map, loc, src_loc, dest_loc, power_name):
""" Issues a support move order
+
:param xml_map: The xml map being generated
:param loc: The location of the unit sending support (e.g. 'BER')
:param src_loc: The location where the unit is moving from (e.g. 'PAR')
@@ -560,6 +580,7 @@ class Renderer():
def _issue_convoy_order(self, xml_map, loc, src_loc, dest_loc, power_name):
""" Issues a convoy order
+
:param xml_map: The xml map being generated
:param loc: The location of the unit convoying (e.g. 'BER')
:param src_loc: The location where the unit being convoyed is moving from (e.g. 'PAR')
@@ -666,6 +687,7 @@ class Renderer():
def _issue_build_order(self, xml_map, unit_type, loc, power_name):
""" Adds a build army/fleet order to the map
+
:param xml_map: The xml map being generated
:param unit_type: The unit type to build ('A' or 'F')
:param loc: The province where the army is to be built (e.g. 'PAR')
@@ -711,6 +733,7 @@ class Renderer():
def _issue_disband_order(self, xml_map, loc):
""" Adds a disband order to the map
+
:param xml_map: The xml map being generated
:param loc: The province where the unit is disbanded (e.g. 'PAR')
:return: Nothing
@@ -740,6 +763,7 @@ class Renderer():
def _center_symbol_around_unit(self, loc, is_dislodged, symbol): # type: (str, bool, str) -> Tuple[str, str]
""" Compute top-left coordinates of a symbol to be centered around a unit.
+
:param loc: unit location (e.g. 'PAR')
:param is_dislodged: boolean to tell if unit is dislodged
:param symbol: symbol identifier (e.g. 'HoldUnit')
@@ -756,6 +780,7 @@ class Renderer():
def _get_unit_center(self, loc, is_dislodged): # type: (str, bool) -> Tuple[float, float]
""" Compute coordinates of unit center.
+
:param loc: unit location
:param is_dislodged: boolean to tell if unit is dislodged
:return: a couple of coordinates (x, y) as floating values
@@ -769,12 +794,14 @@ class Renderer():
def _plain_stroke_width(self): # type: () -> float
""" Return generic stroke width for plain lines.
+
:return: stroke width as floating value.
"""
return float(self.metadata['symbol_size']['Stroke'][0])
def _colored_stroke_width(self): # type: () -> float
""" Return generic stroke width for colored or textured lines.
+
:return: stroke width as floating value.
"""
return float(self.metadata['symbol_size']['Stroke'][1])