diff options
Diffstat (limited to 'diplomacy/utils/export.py')
-rw-r--r-- | diplomacy/utils/export.py | 164 |
1 files changed, 164 insertions, 0 deletions
diff --git a/diplomacy/utils/export.py b/diplomacy/utils/export.py new file mode 100644 index 0000000..459313d --- /dev/null +++ b/diplomacy/utils/export.py @@ -0,0 +1,164 @@ +# ============================================================================== +# Copyright (C) 2019 - Philip Paquette, Steven Bocco +# +# This program is free software: you can redistribute it and/or modify it under +# the terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) any +# later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more +# details. +# +# You should have received a copy of the GNU Affero General Public License along +# with this program. If not, see <https://www.gnu.org/licenses/>. +# ============================================================================== +""" Exporter + - Responsible for exporting games in a standardized format to disk +""" +from diplomacy.engine.game import Game +from diplomacy.engine.map import Map +from diplomacy.utils.game_phase_data import GamePhaseData + +# Constants +RULES_TO_SKIP = ['SOLITAIRE', 'NO_DEADLINE', 'CD_DUMMIES', 'ALWAYS_WAIT', 'IGNORE_ERRORS'] + +def to_saved_game_format(game): + """ Converts a game to a standardized JSON format + :param game: game to convert. + :return: A game in the standard JSON format used to saved game (returned object is a dictionary) + :type game: Game + """ + + # Get phase history. + phases = game.get_phase_history() + # Add current game phase. + phases.append(game.get_phase_data()) + # Filter rules. + rules = [rule for rule in game.rules if rule not in RULES_TO_SKIP] + # Extend states fields. + phases_to_dict = [phase.to_dict() for phase in phases] + for phase_dct in phases_to_dict: + phase_dct['state']['game_id'] = game.game_id + phase_dct['state']['map'] = game.map_name + phase_dct['state']['rules'] = rules + + # Building saved game + return {'id': game.game_id, + 'map': game.map_name, + 'rules': rules, + 'phases': phases_to_dict} + +def is_valid_saved_game(saved_game): + """ Checks if the saved game is valid. + This is an expensive operation because it replays the game. + :param saved_game: The saved game (from to_saved_game_format) + :return: A boolean that indicates if the game is valid + """ + # pylint: disable=too-many-return-statements, too-many-nested-blocks, too-many-branches + nb_forced_phases = 0 + max_nb_forced_phases = 1 if 'DIFFERENT_ADJUDICATION' in saved_game.get('rules', []) else 0 + + # Validating default fields + if 'id' not in saved_game or not saved_game['id']: + return False + if 'map' not in saved_game: + return False + map_object = Map(saved_game['map']) + if map_object.name != saved_game['map']: + return False + if 'rules' not in saved_game: + return False + if 'phases' not in saved_game: + return False + + # Validating each phase + nb_messages = 0 + nb_phases = len(saved_game['phases']) + last_time_sent = -1 + for phase_ix in range(nb_phases): + current_phase = saved_game['phases'][phase_ix] + state = current_phase['state'] + phase_orders = current_phase['orders'] + previous_phase_name = 'FORMING' if phase_ix == 0 else saved_game['phases'][phase_ix - 1]['name'] + next_phase_name = 'COMPLETED' if phase_ix == nb_phases - 1 else saved_game['phases'][phase_ix + 1]['name'] + power_names = list(state['units'].keys()) + + # Validating messages + for message in saved_game['phases'][phase_ix]['messages']: + nb_messages += 1 + if map_object.compare_phases(previous_phase_name, message['phase']) >= 0: + return False + if map_object.compare_phases(message['phase'], next_phase_name) > 0: + return False + if message['sender'] not in power_names + ['SYSTEM']: + return False + if message['recipient'] not in power_names + ['GLOBAL']: + return False + if message['time_sent'] < last_time_sent: + return False + last_time_sent = message['time_sent'] + + # Validating phase + if phase_ix < (nb_phases - 1): + is_forced_phase = False + + # Setting game state + game = Game(saved_game['id'], map_name=saved_game['map'], rules=['SOLITAIRE'] + saved_game['rules']) + game.set_phase_data(GamePhaseData.from_dict(current_phase)) + + # Determining what phase we should expect from the dataset. + next_state = saved_game['phases'][phase_ix + 1]['state'] + + # Setting orders + game.clear_orders() + for power_name in phase_orders: + game.set_orders(power_name, phase_orders[power_name]) + + # Validating orders + orders = game.get_orders() + for power_name in orders: + if sorted(orders[power_name]) != sorted(current_phase['orders'][power_name]): + return False + if 'NO_CHECK' not in game.rules: + for order in orders[power_name]: + loc = order.split()[1] + if order not in game.get_all_possible_orders(loc): + return False + + # Validating resulting state + game.process() + + # Checking phase name + if game.get_current_phase() != next_state['name']: + is_forced_phase = True + + # Checking zobrist hash + if game.get_hash() != next_state['zobrist_hash']: + is_forced_phase = True + + # Checking units + units = game.get_units() + for power_name in units: + if sorted(units[power_name]) != sorted(next_state['units'][power_name]): + is_forced_phase = True + + # Checking centers + centers = game.get_centers() + for power_name in centers: + if sorted(centers[power_name]) != sorted(next_state['centers'][power_name]): + is_forced_phase = True + + # Allowing 1 forced phase if DIFFERENT_ADJUDICATION is in rule + if is_forced_phase: + nb_forced_phases += 1 + if nb_forced_phases > max_nb_forced_phases: + return False + + # Making sure NO_PRESS is not set + if 'NO_PRESS' in saved_game['rules'] and nb_messages > 0: + return False + + # The data is valid + return True |